SharePoint OPTIONS preflight request - sharepoint

I have a Service Stack service hosted within a SharePoint 2013 site. When attempting to make a cross domain request to one of the services, a preflight OPTIONS request is made, as expected.
The problem is that the response always comes back as 401 Unauthorized, due to the fact that authentication info is not sent across with the request. I have tried putting some request filters via servicestack to try and bypass the authentication, but these filters are not firing - it seems like something prior to service stack is sending the response.
Is there any way of specifying that OPTIONS requests to the sharepoint site do not need to be authenticated? If not, does anyone have a workaround for this scenario?
I tried 'fooling' the browser in to not sending a preflight request by changing the data type from application/json to text/plain in my ajax request, but then the data I send is not being deserialised in to the correct RequestDTO for the service calls on the server side.
Any help would be appreciated.

We ended up having to write our own HTTP module in order to support the options request. We basically add a key specifying which domains to allow the CORS requests from (can support more than one) and then have this HTTP module registered:
public class ECSPreFlightModule : IHttpModule
{
/// <summary>
/// You will need to configure this module in the Web.config file of your
/// web and register it with IIS before being able to use it. For more information
/// see the following link: http://go.microsoft.com/?linkid=8101007
/// </summary>
public void Dispose()
{
//clean-up code here.
}
private const string OptionsHeader = "OPTIONS";
private const string OriginHeader = "ORIGIN";
private const string AccessAllowOrigin = "Access-Control-Allow-Origin";
private string AllowedOriginUrlsArray
{
get
{
return GetWebConfigValue("CORSAllowedOriginUrls");
}
}
private string GetWebConfigValue(string key)
{
var configuration = WebConfigurationManager.OpenWebConfiguration("~");
object o = configuration.GetSection("system.web/httpModules");
var section = o as HttpModulesSection;
return section.CurrentConfiguration.AppSettings.Settings[key].Value;
}
public void Init(HttpApplication context)
{
context.PreSendRequestHeaders += (sender, e) =>
{
var splitUrls = AllowedOriginUrlsArray.Split('|');
var response = context.Response;
var originHeader = context.Request.Headers.Get(OriginHeader);
if (!String.IsNullOrEmpty(originHeader) && splitUrls.Length > 0)
{
foreach (var url in splitUrls)
{
var urlLower = url.ToLower();
var originHeaderLower = originHeader.ToLower();
// if the method being requested is an OPTIONS request and the url is the url specified in the web.config then return an OK response.
if (context.Request.HttpMethod.ToLowerInvariant() == OptionsHeader.ToLowerInvariant() &&
(urlLower == originHeaderLower))
{
response.StatusCode = (int)HttpStatusCode.OK;
}
// If the originating header url is equal to the url specified in the web.config then grant the access control
if (originHeaderLower == urlLower)
{
response.AddHeader(AccessAllowOrigin, originHeader);
break;
}
}
}
};
}
}
}
The above module was wrapped in a sharepoint feature that, when activated, made the appropriate changes to the web.config, namely registering the module and adding the following keys:
<add name='Access-Control-Allow-Credentials' value='true' />
<add name='Access-Control-Allow-Headers' value='Authorization, X-Requested-With, Content-Type, Origin, Accept, X-RequestDigest' />
<add name='Access-Control-Allow-Methods' value='GET,POST,OPTIONS,PUT, DELETE' />

Related

Setting Different ReturnUrl in State Parameter with Microsoft.Identity.Web

I have an ASP.NET Core web app that is authenticating with Azure AD in a multi-tenant configuration using Microsoft.Identity.Web. We use a tenant/company identifier as the subdomain of our apps URL. (companyA.myapp.com, companyB.myapp.com). Some users have access to more than one tenant of the application, so we cannot map a Azure AD tenant directly to a single tenant/company in our app.
With Microsoft.Identity.Web, how is the state parameter set or manipulated as described here? I would like to follow the guidance provided here, but am not sure where to start.
https://learn.microsoft.com/en-us/azure/active-directory/develop/reply-url#use-a-state-parameter
If you have several subdomains and your scenario requires that, upon successful authentication, you redirect users to the same page from which they started, using a state parameter might be helpful.
In this approach:
Create a "shared" redirect URI per application to process the security tokens you receive from the authorization endpoint.
Your application can send application-specific parameters (such as subdomain URL where the user originated or anything like branding information) in the state parameter. When using a state parameter, guard against CSRF protection as specified in section 10.12 of RFC 6749).
The application-specific parameters will include all the information needed for the application to render the correct experience for the user, that is, construct the appropriate application state. The Azure AD authorization endpoint strips HTML from the state parameter so make sure you are not passing HTML content in this parameter.
When Azure AD sends a response to the "shared" redirect URI, it will send the state parameter back to the application.
The application can then use the value in the state parameter to determine which URL to further send the user to. Make sure you validate for CSRF protection.
Here is how I eventually solved the the MS Login infinite redirects with the tenant per subdomain scheme problem. (Trying to come up with a better name for the problem. 😁)
Provide a SigninRedirect controller action that accepts a returnUrl parameter that we must validate to avoid being an Open Redirect.
Example URL with returnUrl set to companyA.example.com/foo&bar=1:
https://signin.example.com/signin-redirect?returnUrl=companyA.example.com%2Ffoo%26bar%3D1
[Route("")]
public class SigninController : Controller
{
private readonly IMediator _mediator;
private readonly IConfiguration _configuration;
public SigninController(IMediator mediator, IConfiguration configuration)
{
_mediator = mediator;
_configuration = configuration;
}
[Authorize]
[HttpGet("/signin-redirect")]
public IActionResult SigninRedirect(string returnUrl)
{
string redirect;
if (!string.IsNullOrEmpty(returnUrl) && IsValidSubdomainUrl(returnUrl))
{
redirect = returnUrl;
}
else
{
var appHost = _configuration.GetValue<string>("General:ApplicationHost");
var home = new UriBuilder("https", appHost).Uri;
return Redirect(home.AbsoluteUri);
}
return Redirect(redirect);
}
/// <summary>
/// Avoid Open Redirect Vulnerability
/// https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
/// </summary>
/// <param name="returnUrl"></param>
/// <returns></returns>
private bool IsValidSubdomainUrl(string returnUrl)
{
var appHost = _configuration.GetValue<string>("General:ApplicationHost");
var isValid = Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute) &&
Uri.TryCreate(returnUrl, UriKind.Absolute, out var uri) &&
uri?.Host.EndsWith(appHost) == true;
return isValid;
}
}
In ConfigureServices(IServiceCollection services) set cookies to be shared across all the subdomains and configure an OnRedirectToIdentityProvider event to redirect to your signin URL when the user is not yet authenticated:
var cookieDomain = _configuration.GetValue<string>("General:CookieDomain");
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromDays(3);
options.Cookie.Domain = cookieDomain;
options.Cookie.Path = "/";
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
_configuration.Bind("AzureAd", options);
options.Events ??= new OpenIdConnectEvents();
options.Events.OnRedirectToIdentityProvider += context => {
var originalRequestUri = context.HttpContext.Request.GetUri();
var signInHost = _configuration.GetValue<string>("General:SignInHost");
var signInPath = _configuration.GetValue<string>("General:SignInUrl");
if (!originalRequestUri.Host.Equals(signInHost, StringComparison.InvariantCultureIgnoreCase))
{
var signInUrl = QueryHelpers.AddQueryString(signInPath, "returnUrl", originalRequestUri.AbsoluteUri);
// When on a subdomain and not authorized, then redirect to
// our signin URL.
context.Response.Redirect(signInUrl);
// Let Microsoft.Identity.Web know that we already handled
// this redirect
context.HandleResponse();
}
return Task.CompletedTask;
};
},
cookieOptions =>
{
cookieOptions.Cookie.Domain = cookieDomain;
cookieOptions.Cookie.Path = "/";
cookieOptions.Cookie.SameSite = SameSiteMode.Lax;
})
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "user.read" })
.AddDistributedTokenCaches();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Domain = cookieDomain;
options.Cookie.Name = ".AspNet.SharedCookie";
options.Cookie.Path = "/";
options.Cookie.SameSite = SameSiteMode.Lax;
});
Configuration defined:
"General:CookieDomain": ".example.com",
"General:ApplicationHost": "example.com",
"General:SignInHost": "signin.example.com",
"General:SignInUrl": "https://signin.example.com/signin-redirect"
In addition, you will need the usual AzureAd configuration section from the Microsoft Docs for Azure AD.
"AzureAd:Instance": "https://login.microsoftonline.com/",
"AzureAd:Domain": "...",
"AzureAd:TenantId": "common",
"AzureAd:ClientId": "...",
"AzureAd:ClientSecret": "...",
"AzureAd:CallbackPath": "/signin-oidc",
"AzureAd:SignedOutCallbackPath": "/signout-callback-oidc",

Using Azure B2C with an MVC app gets into infinite loop resulting with Bad Request - Request Too Long Http 400 error

So I've built and published a new website that uses Azure B2C as the authentication mechanism.
What I found was that the login and sign would work fine for a while. But after a period of time, say couple of hours after visiting the site post deployment, I would find that on login or signup, after successful authentication, instead of being redirected back to the return url set up in the b2c configuration, my browser would get caught between an infinite loop between the post authentication landing page that is protected with an authorise attribute and the Azure B2C Login page, before finally finishing with Http 400 error message with the message - Bad Request - Request too long.
I did some googling around this and there are number of posts that suggest that the problem is with the cookie, and that deleting the cookie should resolve the issue. This is not the case. The only thing I have found to fix this is restarting the application on the webserver, or waiting say 24 hours for some kind of cache or application pool to reset. Anyone has any ideas what's going on here?
Ok, I think I may have found the answer.
Looks like there is an issue with Microsoft.Owin library and the way it sets cookies. Writing directly to System.Web solves this problem according to this article.
There are three suggested solutions:
Ensure session is established prior to authentication: The conflict between System.Web and Katana cookies is per request, so it may be possible for the application to establish the session on some request prior to the authentication flow. This should be easy to do when the user first arrives, but it may be harder to guarantee later when the session or auth cookies expire and/or need to be refreshed.
Disable the SessionStateModule: If the application is not relying on session information, but the session module is still setting a cookie that causes the above conflict, then you may consider disabling the session state module.
Reconfigure the CookieAuthenticationMiddleware to write directly to System.Web's cookie collection.
I will opt for the third option, which is to overwrite the default Cookie AuthenticationMiddleware, as they have suggested below.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ...
CookieManager = new SystemWebCookieManager()
});
public class SystemWebCookieManager : ICookieManager
{
public string GetRequestCookie(IOwinContext context, string key)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
var cookie = webContext.Request.Cookies[key];
return cookie == null ? null : cookie.Value;
}
public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
var cookie = new HttpCookie(key, value);
if (domainHasValue)
{
cookie.Domain = options.Domain;
}
if (pathHasValue)
{
cookie.Path = options.Path;
}
if (expiresHasValue)
{
cookie.Expires = options.Expires.Value;
}
if (options.Secure)
{
cookie.Secure = true;
}
if (options.HttpOnly)
{
cookie.HttpOnly = true;
}
webContext.Response.AppendCookie(cookie);
}
public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (options == null)
{
throw new ArgumentNullException("options");
}
AppendResponseCookie(
context,
key,
string.Empty,
new CookieOptions
{
Path = options.Path,
Domain = options.Domain,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
}
I will give that a crack, and post my results back here.

Swagger authentication in Azure App Service

In my Azure Mobile .NET backend I want to use Azure Mobile .NET Server Swagger . I'm looking for fast way to hide swagger UI from public access ? Is there any way to provide access only for selected users ?
First a disclaimer: Even if you protect your Swagger UI from public consumption, you are not protecting your APIs from public consumption. You have to assume that everyone knows all of your routes and have the appropriate security in place to protect any requests that may come in.
That being said, there's still not a simple way to do this. Swashbuckle (the piece that adds Swagger to Web API) adds a custom HttpMessageHandler to the /swagger/ui route (as seen here). If you look at the Web API pipeline, you can see that if you specify a custom handler, you can bypass all of the Controller selection, Auth filters, etc. This is what happens here.
Some solutions:
Use an app setting to conditionally call ConfigureSwagger(config) in debug modes only. This would prevent all /swagger routes from making it into production. Or you could use a staging slot and only add it there.
You can wrap the SwaggerUiHandler with something like this Basic Auth MessageHandler. This would prompt the user for basic creds if they went to the /swagger/ui route. See below for my modified version of this code.
Maybe with a little more thought we can come up with a better solution -- I see a couple of issues (here and here) in the Swashbuckle repo that indicate you're not the first one to hit this.
Modified BasicAuthHandler (from here):
Warning: minimally tested (and be sure to change how you verify user/pass)
public class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic";
public BasicAuthMessageHandler(HttpMessageHandler innerHandler)
{
this.InnerHandler = innerHandler;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AuthenticationHeaderValue authValue = request.Headers.Authorization;
HttpResponseMessage unauthorizedResponse = request.CreateUnauthorizedResponse();
if (authValue != null && !string.IsNullOrWhiteSpace(authValue.Parameter))
{
Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);
if (parsedCredentials != null)
{
// TODO: Check that the user/pass are valid
if (parsedCredentials.Username == "user" &&
parsedCredentials.Password == "pass")
{
// If match, pass along to the inner handler
return base.SendAsync(request, cancellationToken);
}
}
}
else
{
// Prompt for creds
unauthorizedResponse.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
}
return Task.FromResult(unauthorizedResponse);
}
private Credentials ParseAuthorizationHeader(string authHeader)
{
string[] credentials = Encoding.ASCII.GetString(Convert
.FromBase64String(authHeader))
.Split(
new[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0])
|| string.IsNullOrEmpty(credentials[1])) return null;
return new Credentials()
{
Username = credentials[0],
Password = credentials[1],
};
}
}
Registering with Swagger route
// Do this after calling ConfigureSwagger
ConfigureSwagger(config);
// Remove the swagger_ui route and re-add it with the wrapped handler.
var route = config.Routes["swagger_ui"];
config.Routes.Remove("swagger_ui");
config.Routes.MapHttpRoute("swagger_ui", route.RouteTemplate, route.Defaults, route.Constraints, new BasicAuthMessageHandler(route.Handler));

Is it possible to enable CORS using NancyFX?

I have an API service made with NancyFX, and a couple of front-end developers creating an SPA JS client against this API.
We would like to test the client side code against the published server without having to publish the client code with too much frequency.
But, the client runs at localhost, and the server is at Windows Azure.
Is it possible and easy to enable CORS on the NancyFX server? How can I do that?
Thanks.
Its possible to do this in the bootstraper of Nancy
protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
{
//CORS Enable
pipelines.AfterRequest.AddItemToEndOfPipeline((ctx) =>
{
ctx.Response.WithHeader("Access-Control-Allow-Origin", "*")
.WithHeader("Access-Control-Allow-Methods", "POST,GET")
.WithHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-type");
});
If you're using IIS to host Nancy, in this case on Windows Azure then you can just update the web.config to add the header to every request.
This can be done by adding the following:
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
</customHeaders>
</httpProtocol>
</system.webServer>
Alternatively you can do what Sunny suggested, and if you don't like writing that every time you can add your own extension method:
public static class NancyExtensions
{
public static void EnableCors(this NancyModule module)
{
module.After.AddItemToEndOfPipeline(x =>
{
x.Response.WithHeader("Access-Control-Allow-Origin", "*");
});
}
}
Then you can just call this.EnableCors() in your route.
If your HTTP request is simple then Phill's answer will suffice, but if the request is not so simple, the browser will send a preflight check. The preflight check is an OPTIONS HTTP request and this has to be handled too.
Here is an extension method to configure CORS:
public static class MyNancyExtension
{
public static void EnableCORS(this Nancy.Bootstrapper.IPipelines pipelines)
{
pipelines.AfterRequest.AddItemToEndOfPipeline(ctx =>
{
if (ctx.Request.Headers.Keys.Contains("Origin"))
{
var origins = "" + string.Join(" ", ctx.Request.Headers["Origin"]);
ctx.Response.Headers["Access-Control-Allow-Origin"] = origins;
if (ctx.Request.Method == "OPTIONS")
{
// handle CORS preflight request
ctx.Response.Headers["Access-Control-Allow-Methods"] =
"GET, POST, PUT, DELETE, OPTIONS";
if (ctx.Request.Headers.Keys.Contains("Access-Control-Request-Headers"))
{
var allowedHeaders = "" + string.Join(
", ", ctx.Request.Headers["Access-Control-Request-Headers"]);
ctx.Response.Headers["Access-Control-Allow-Headers"] = allowedHeaders;
}
}
}
});
}
}
To enable CORS call this extension method in the bootstrapper:
protected override void ApplicationStartup(Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
pipelines.EnableCORS();
}
Please note it is not extending NancyModule because OPTIONS is handled outside of module (also here).

How to propage WebSphere security tokens when calling HTTP from EJB

I have an EJB which makes a call to another server in the cell using HTTP (REST api).
At the EJB context the user is already authenticated and authorized, how can I propagate the security tokens to the other server avoiding the need to provide credentials in the request ?
It is possible to obtain WebSphere's Ltpa token from the security subject and pass it as a cookie for the HTTP call:
public static SingleSignonToken getSSOTokenFromSubject(final Subject subject) {
if (subject == null) {
return null;
}
return AccessController.doPrivileged(new PrivilegedAction<SingleSignonToken>() {
public SingleSignonToken run() {
Set<SingleSignonToken> ssoTokens = subject.getPrivateCredentials(SingleSignonToken.class);
for (SingleSignonToken ssoToken : ssoTokens) {
if (ssoToken.getName().equals("LtpaToken")) {
return ssoToken;
}
}
return null;
}
});
}
// Get cookie to add to outgoing HTTP requests
SingleSignonToken ssoToken = getSSOTokenFromSubject(subject);
String ssoTokenStr = null;
if (ssoToken != null) {
byte[] ssoTokenBytes = ssoToken.getBytes();
ssoTokenStr = com.ibm.ws.util.Base64.encode(ssoTokenBytes);
}
String ssoTokenCookie = "LtpaToken2=" + ssoTokenStr;
By adding the ssoTokenCookie to the request cookies there is no need to provider user credentials.
Cookie ltpaCookie = WebSecurityHelper.getSSOCookieFromSSOToken();
Extracts the SSO token from the subject of current thread and builds an SSO cookie out of it for use on downstream web invocations. Basically what the whole code in the post below does. This method is accessible from WAS 8.x I believe.
Following Jar is needed as compile reference:
com.ibm.ws.admin.client-8.5.0.jar
(I'm using WAS 8.5.5.11 for this example)

Resources