Summary
How to create an HTTPContext within a webservice? or POST to a Handler.ashx from a webservice?
Background
I have a Cold Fusion web application that uses Forms authentication but somehow achieves Windows authentication with this script:
<cfscript>
ws = CreateObject("webservice", "#qTrim.webServiceName#");
ws.setUsername("#qTrim.trimAcct#");
ws.setPassword("#qTrim.trimpwd#");
wsString=ws.UploadFileCF("#qTrim.webserviceurl#","#objBinaryData#", "#qFiles.Filename#", "Document", "#MetaData#");
</cfscript>
Apparently, the setUsername/setPassword values map to a single Windows domain account and this works in production. (The webservice is written in C# and built with .Net 4.0. and it must be used by this domain account)
I developed a DownloadHandler.ashx which works when POSTed to by a process which is running under this domain account (I have a .Net web client with a button that defines PostBackUrl="~/DownloadHandler.ashx"). This HTTPHandler grabs a few items from the HTTPContext and then calls the above webservice method DownloadFile without problems.
My Problem
Now this ColdFusion app needs to download a file using this webservice. When the CF code POSTs an HTML form to the DownloadHandler.ashx it works - BUT ONLY IF the CF tester is using this Windows domain account. This won't work in production because the CF app supports remote anonymous users through forms authentication.
Question
Not knowing ColdFusion myself, I was thinking of the following changes:
Replicate the above CF technique such that user/pswd can be set the same and have CF invoke the ws.DownloadFile method directly
I think this would require using most of my current HTTPHandler code in my webservice but I cannot think of how to handle the output. When this handler is POSTed to, it prompts for OPEN or Save and works nicely but I'm confused on how I would stream this back from the webservice itself.
The current DownloadFile webmethod communicates with a database product and returns output to this (the current) handler:
Code
namespace WebClient
{
public class DownloadHandler : IHttpHandler
{
ASMXproxy.FileService brokerService;
public void ProcessRequest(HttpContext context)
{
brokerService = new ASMXproxy.FileService();
string recNumber = context.Request.Form["txtRecordNumber"];
brokerService.Url = context.Request.Form["txtURL"];
string trimURL = context.Request.Form["txtFakeURLParm"]; // not a real URL but parms to connect to TRIM
brokerService.Timeout = 9999999;
brokerService.Credentials = System.Net.CredentialCache.DefaultCredentials;
byte[] docContent;
string fileType;
string fileName;
string msgInfo = brokerService.DownloadFile(trimURL, recNumber, out docContent, out fileType, out fileName);
string ContentType = MIMEType.MimeType(fileType);
context.Response.AppendHeader("Content-Length", docContent.Length.ToString());
context.Response.AppendHeader("content-disposition", "attachment; filename=\"" + fileName + "\"");
context.Response.ContentType = ContentType;
context.Response.OutputStream.Write(docContent, 0, docContent.Length);
context.Response.OutputStream.Flush();
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
Assuming your CF site is running on IIS and not Apache or some other web server, this might work:
Put your .cfm file that calls the webservice into its own subfolder on your site. Set the Authentication properties of that folder to use Anonymous Authentication, but set the user identity to the Windows Domain account that successfully calls the webservice (click the Set... button on the dialog shown below and enter the appropriate credentials).
Related
We have an Outlook Add-In which runs in OWA.
The Manifest sits on https://company.ourdomain.com
The Javascript sits on https://company.ourdomain.com
The Custom Web Service we wrote in-house sits on https://company.ourdomain.com
When I make a call from within JavaScript in response to an Add-In Command, I use the format https://company.ourdomain.com/api/Controller/Action in the ajax call.
I end up getting one of those CORS errors (sometimes it's pre-flight, other times CORB). Why am I getting this if the Javascript is literally sitting on the same domain as the web service?
I'm assuming I'm authenticated since I've logged into my Outlook account.
What gives?
NOTE:
As an experiment I attempted a RESTful call by directly typing in the URL (No OWA involved). This caused the code to Authenticate against Azure AD. Then afterward I logged into OWA in the same browser session and everything worked fine. Do I actually need to authenticate within the Javascript even if the webservice I'm calling is in the same domain?
AJAX CALL WHICH GENERATES ERROR
Remember, it will work just fine after I've made a RESTful call by making a call to my web service directly from the Browser
var apiUri = '/api/People/ShowRecord';
$.ajax({
url: apiUri,
type: 'POST',
data: JSON.stringify(serviceRequest),
contentType: 'application/json; charset=utf-8',
dataType: 'json'
}).done(function (response) {
if (!response.isError) {
// response to successful call
}
else {
// ...
}
}).fail(function (status) {
// some other response
}).always(function () {
console.log("Completed");
});
OBSERVATION
When I call the api from the Address Bar the code below is run. This code never gets invoked by Javascript
[assembly: OwinStartup(typeof(EEWService.AuthStartup))]
namespace EEWService
{
public partial class AuthStartup
{
public void Configuration(IAppBuilder app)
{ app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Notifications = new WsFederationAuthenticationNotifications
{
RedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.Whr = "ourdomain.com";
return Task.FromResult(0);
}
},
MetadataAddress = ConfigurationManager.AppSettings["ida:MetadataAddress"],
Wtrealm = ConfigurationManager.AppSettings["ida:Audience"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = new string[] { $"spn:{ConfigurationManager.AppSettings["ida:Audience"]}" }
}
});
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
MetadataAddress = ConfigurationManager.AppSettings["ida:MetadataAddress"],
});
}
}
}
There are a few problems with this I think.
The first one is you are trying to serve your static content off the same server you are serving your code from. This is in general considered a bad-practice, purely because no point in wasting those precious server resources for static content. Ideally you should upload your static content to a CDN - and let the users' browser make a request to some super-cached file server. However - I understand this option might not be available to you as of now. This also isn't the root cause.
The second and the real problem is, (you think you are but) you are not authenticated. Authentication in Outlook web-addins doesn't come by default, it's something you need to handle. When Outlook loads your web add-in into the side panel it makes certain methods available to you which you can use and kind-of create a pseudo-identity (as an example Office.context.mailbox.userProfile.emailAddress ) - but if you want real authentication, you will need to do that yourself.
There are three ways of doing that as far as I can tell.
The first one is through the Exchange Identity Token
Second one is through the Single Sign On feature
The third one - which I think is the most convenient and the simplest in logic to implement is using WebSockets. (SignalR might be what you need).
When the user loads your first page, make sure a JS value like window.Unique_ID available to them. This will come in handy.
Have a button in your UI - which reads "Authenticate"
When the user clicks to this button, you pop them out to a url which will redirect to your authentication URL. (Something like https://company.ourdomain.com/redirToAuth). This would save you the trouble of getting blocked in the side-panel, because you are using window.open with a url that's on your domain. Pass that Unique_ID to redirection which then redirects you to OAuth login URL. That should look like https://login.microsoftonline.com/......&state=Unique_ID
Right after popping the user to sign in window, in your main JS (which is client-side), you open a web-socket to your server, again with that Unique_ID and start listening.
When the user completes authentication, the OAuth flow should post back either an access token, or the code. If you get the access token, you can send it through the sockets to front-end (using the Unique_ID which is in the parameters of post-back) or if you had the code, you finish authenticating the user with a server-to-server call and pass the access token the same way afterwards. So you use that unique Id to track the socket that user connected from and relay access token to only that user.
I would like other web application (in .net or any other) to call my JAX-RS web service to set and open my JSF page with some passed values.
E.g.
Another web application just pass the userId and a token to open an entry page.
User will complete the entry and service will return the uniqueId for created entry.
I am confused in how I can set JSF context parameters and open that JSF page using JAX-RS. Can anyone give idea about how to set value of JSF session scoped managed bean using web service?
First of all, this question indicates a misunderstanding of purpose of "REST web services" in general. The question concretely asks to perform two rather unusual tasks with a REST web service:
Manipulating the HTTP session associated with the request.
Redirecting to a HTML page managed by a stateful MVC framework as response.
Both squarely contradict the stateless nature of REST. Those tasks aren't supposed to be performed by a REST web service. Moreover, REST web services are primarily intented to be used by programmatic clients (e.g. JavaScript or Java code), not by webbrowsers which consume HTML pages. You normally don't enter the URL of a REST webservice in browser's address bar in order to see a HTML page. You normally enter the URL of a HTML page in browser's address bar. That HTML page can in turn be produced by a HTML form based MVC framework such as JSF.
In your specific case, after the programmatic client has retrieved the unique
ID from the REST web service, then the programmatic client should in turn all by itself fire a new request to the JSF web application. E.g. as follows in Java based client (below example assumes it's a plain servlet, but it can be anything else as you said yourself):
String uniqueId = restClient.getUniqueId(userId, token);
String url = "http://example.com/context/login.xhtml?uniqueId=" + URLEncoder.encode(uniqueId, "UTF-8");
response.sendRedirect(url);
In the target JSF web application, just use <f:viewParam>/<f:viewAction> the usual way in order to grab the unique ID and perform business actions based on that. E.g. as below in login.xhtml:
<f:metadata>
<f:viewParam name="uniqueId" value="#{authenticator.uniqueId}" />
<f:viewAction action="#{authenticator.check}" />
</f:metadata>
#ManagedBean
#RequestScoped
public class Authenticator {
private String uniqueId;
#EJB
private UserService service;
public String check() {
FacesContext context = FacesContext.getCurrentInstance();
User user = service.authenticate(uniqueId);
if (user != null) {
context.getExternalContext().getSessionMap().put("user", user);
return "/somehome.xhtml?faces-redirect=true";
}
else {
context.addMessage(null, new FacesMessage("Invalid token"));
return null; // Or some error page.
}
}
// Getter/setter.
}
Perhaps the REST webservice could for convenience even return the full URL including the unique ID so that the client doesn't need to worry about the target URL.
String uniqueIdURL = restClient.getUniqueIdURL(userId, token);
response.sendRedirect(uniqueIdURL);
On the other hand, there's a reasonable chance that you just misunderstood the functional requirement and you can just directly process the user ID and token in the JSF web application, the more likely if it runs at the same server as the REST web service and also uses HTTPS. Just add an extra <f:viewParam> and do the same business service logic as the JAX-RS resource in the <f:viewAction> method.
<f:metadata>
<f:viewParam name="userId" value="#{authenticator.userId}" />
<f:viewParam name="token" value="#{authenticator.token}" />
<f:viewAction action="#{authenticator.check}" />
</f:metadata>
See also:
What can <f:metadata>, <f:viewParam> and <f:viewAction> be used for?
How to implement JAX-RS RESTful service in JSF framework
Try this in your code,
return new Viewable("/index", "FOO");
Or,
RequestDispatcher rd = request.getRequestDispatcher("{url}");
rd.forward(request, response);
Or,
return Response.status(myCode).entity(new Viewable("/index", "FOO")).build();
try these example working in using jersey,
#GET
public Response get() {
URI uri=new URI("http://nohost/context");
Viewable viewable=new Viewable("/index", "FOO");
return Response.ok(viewable).build();
}
to return something different use this approach:
#GET
public Response get() {
int statusCode=204;
Viewable myViewable=new Viewable("/index","FOO");
return Response.status(statusCode).entity(myViewable).build();
}
Hope that helpe
In a standalone (selfhosted) application, I would like to have an httpserver that on a single base adress can either serve simple web pages (without any serverside dynamics/scripting, it just returns the content request files) or serve RESTful webservices:
when http://localhost:8070/{filePath} is requested, it should return the content of the file (html, javascript, css, images), just like a normal simple webserver
everything behind http://localhost:8070/api/ should just act as a normal RRESTful Web API
My current approach uses ASP.NET Web API to server both the html pages and the REST APIs:
var config = new HttpSelfHostConfiguration("http://localhost:8070/");
config.Formatters.Add(new WebFormatter());
config.Routes.MapHttpRoute(
name: "Default Web",
routeTemplate: "{fileName}",
defaults: new { controller = "web", fileName = RouteParameter.Optional });
config.Routes.MapHttpRoute(
name: "Default API",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
The WebController is the controller that serves the web pages with this naive code:
public class WebController : ApiController
{
public HttpResponseMessage Get(string fileName = null)
{
/// ...
var filePath = Path.Combine(wwwRoot, fileName);
if (File.Exists(filePath))
{
if (HasCssExtension(filePath))
{
return this.Request.CreateResponse(
HttpStatusCode.OK,
GetFileContent(filePath),
"text/css");
}
if (HasJavaScriptExtension(filePath))
{
return this.Request.CreateResponse(
HttpStatusCode.OK,
GetFileContent(filePath),
"application/javascript");
}
return this.Request.CreateResponse(
HttpStatusCode.OK,
GetFileContent(filePath),
"text/html");
}
return this.Request.CreateResponse(
HttpStatusCode.NotFound,
this.GetFileContnet(Path.Combine(wwwRoot, "404.html")),
"text/html");
}
}
And again, for everything behind /api, controllers for normal REST APIs are used.
My question now is: Am I on the right track? I kind of feel that I am rebuilding a webserver here, reinventing the wheel. And I guess that there are probably a lot of http request web browsers could make that I do not handle correctly here.
But what other option do I have if I want to self host and at the same time server REST web APIs and web pages over the same base address?
Looks like you are trying to recreate asp.net FileHandler for self host. There is a better solution though. Using Katana(an OWIN host) as the hosting layer for web API. OWIN supports hosting multiple OWIN frameworks in the same app. In your case, you can host both web API and a file handler in the same OWIN app.
Filip has a good blog post on this to get you started here. You can use configuration code like this,
public void Configuration(IAppBuilder appBuilder)
{
// configure your web api.
var config = new HttpConfiguration();
config.Routes.MapHttpRoute("default-api", "api/{controller}");
appBuilder.UseWebApi(config);
// configure your static file handler.
appBuilder.UseStaticFiles();
}
IMO there is nothing wrong with what you are doing. I use self-host for delivering files, html docs as well as being a regular API. At the core, self-host is using HTTP.SYS just as IIS is.
As RaghuRam mentioned there are Owin hosts that have some optimizations for serving static files, but WCF selfhost is quite capable of getting decent perf for serving files.
See this link which uses a more straigftforward approach
Setting app a separate Web API project and ASP.NET app
RouteTable.Routes.IgnoreRoute(".js");
RouteTable.Routes.IgnoreRoute(".html");
I have developed a custom SharePoint web service, and deployed it to /_vti_bin/myservice.asmx. As a "regular" user, browsing to that ASMX URL works fine. When I try to browse to "/_vti_bin/myservice.asmx/js" as required to call this service from ASP.Net AJAX, I get a 403. If I browse to it as no less than a farm admin (site collection admin doesn't work), I get a 403. It is entirely possible that the farm admin's role as a local server admin is also allowing it to work.
This is my web service class:
[WebService(Namespace = "http://sharepointservices.genericnamespace.com/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[System.Web.Script.Services.ScriptService]
public class ApprovalSvc : System.Web.Services.WebService
{
[WebMethod]
[ScriptMethod(ResponseFormat = ResponseFormat.Xml)]
public XmlDocument GetInboxItems(string inboxName, string s_Id)
{
// code removed
}
}
This is the art of my web part code where I am hooking up the ASP.Net AJAX stuff:
ScriptManager scriptMgr = new ScriptManager();
string webUrl = SPContext.Current.Web.Url;
ServiceReference srvRef = new ServiceReference(webUrl + "/_vti_bin/ApprovalSvc.asmx");
scriptMgr.Services.Add(srvRef);
this.Controls.Add(scriptMgr);
If I'm logged in as a farm/server admin, it works. Otherwise, no. The web service assembly is in the GAC & listed in SafeControls. Any ideas?
Good old Process Monitor to the rescue.
The facts:
The service code DLL is in the web application's bin directory, as it cannot be signed b/c it references unsigned DLLs.
The request for the service DLL is coming from ASP.Net & not SharePoint, specifically an HttpModule in the System.Web.Extensions assembly.
The solution:
Because the request didn't come through SharePoint, and identity impersonation is also turned on by default, the default NTLM permissions on the web app's BIN directory were not good enough - the user's account had no access to the BIN directory or the DLLs within it.
We gave the NT AUTHORITY\Authenticated Users Read access (not Read & Execute, not List Folder Contents, just Read) to the folder, and all is well.
I created a very simple webservice in ASP.NET 2.0 to query a list in SharePoint 2007 like this:
namespace WebService1
{
/// <summary>
/// Summary description for Service1
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
// [System.Web.Script.Services.ScriptService]
public class Service1 : System.Web.Services.WebService
{
[WebMethod]
public string HelloWorld()
{
return "Hello World";
}
[WebMethod]
public string ShowSPMyList()
{
string username = this.User.Identity.Name;
return GetList();
}
private string GetList()
{
string resutl = "";
SPSite siteCollection = new SPSite("http://localhost:89");
using (SPWeb web = siteCollection.OpenWeb())
{
SPList mylist = web.Lists["MySPList"];
SPQuery query = new SPQuery();
query.Query = "<Where><Eq><FieldRef Name=\"AssignedTo\"/><Value Type=\"Text\">Ramprasad</Value></Eq></Where>";
SPListItemCollection items = mylist.GetItems(query);
foreach (SPListItem item in items)
{
resutl = resutl + SPEncode.HtmlEncode(item["Title"].ToString());
}
}
return resutl;
}
}
}
This web service runs well when tested using the built-in server of Visual Studio 2008. The username indicates exactly my domain account (domain\myusername).
However when I create a virtual folder to host and launch this web service (still located in the same machine with SP2007), I got the following error when invoking ShowSPMyList() method, at the line to execute OpenWeb(). These are the details of the error:
System.Data.SqlClient.SqlException: Cannot open database "WSS_Content_8887ac57951146a290ca134778ddc3f8" requested by the login. The login failed.
Login failed for user 'NT AUTHORITY\NETWORK SERVICE'.
Does anyone have any idea why this error happens? Why does the web service run fine inside Visual Studio 2008, but not when running stand-alone? I checked and in both cases, the username variable has the same value (domain\myusername).
Thank you very much.
Thank you very much for the replies. I'll look into the documents to see how i can change the settings related to the application pool as suggested.
I want to make clear that i wanted to build a webservice to run outside of sharepoint (but can be deployed on the same server with sharepoint).
Is there any way i can programmatically pass the credentials (another domain account instead of 'NT AUTHORITY\NETWORK SERVICE' by default) to sharepoint when invoking OpenWeb method? I believe if i'm able to do that then i can walkaround the security issue above.
When you create your own custom virtual folder and set it inside the IIS, it's highly possible that the user account who run the application pool of that particular IIS virtual directory is currently set to NT authority\Network Service.
You can check carefully, by looking closely of what is the actual application pool that run that particular IIS virtual directory.
From there, you can go to the "Application Pool" folder and right click, choose Properties. Select the "Identity" tab, and it will show you who is the user account that currently running the application pool.
Alternatively, you can refer to the SharePoint SDK, something similar to ExtractCrmAuthenticationToken in dynamics CRM to extract the Authentication Token ticket.
Or alternatively you can use Network Credential to embed your own custom user id and password.
Hope this helps,
hadi teo
I fully agree with Hadi, if this is something you want to just quickly test, for a proof of concept, you can change the credentials under what the Application pool runs, to a user that has permissions. Or you could use Identity Impersonate setting in your config file.
However resist the temptiation to do this in a production enviroment, use the proper authentication. It will come back, to bite you.
If you need to set this up for production, there is a couple of areas that you want to look at, duplicate SPN's, and deligation probably the most common areas that is not configured correctly. Your error however points to impersanation not happening.
Also make sure you are deploying the web service to its own web site that does not already run SharePoint. If you want the web service to run on the same web site as SharePoint read Creating a Custom Web Service.
You can check what application pool identity SharePoint is using by following the same instructions that Hadi writes, but for an app pool running SharePoint. Make sure to only change the application pool used by your web service and not SharePoint or else other permission errors could occur within SP. (There should be no reason but if you are interested in changing the app pool identity used by SharePoint follow these instructions.)
On solution would be to "impersonate" as the SharePoint System account using the following code:
SPSecurity.RunWithElevatedPrivileges(delegate()
{
// also dispose SPSite
using (SPSite siteCollection = new SPSite("http://localhost:89"))
{
using (SPWeb web = siteCollection.OpenWeb())
{
// ...
}
}
});