I'm having an issue getting an appropriate Access-Control-Allow-Origin header back from the server when I have both JWT Bearer Authentication and CORS enabled on the same service.
When I remove UseJwtBearerAuthentication from the configuration, everything works.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("AllowAllOrigins", builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyHeader();
builder.AllowAnyMethod();
builder.AllowCredentials();
});
});
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
options.Audience = "c2cf422a-a432-2038-b183-cda64e16239e";
options.Authority = "domain.com";
});
app.UseCors("AllowAllOrigins");
app.UseIISPlatformHandler();
app.UseMvc();
}
I've tried to change the ordering for configuration, but nothing seems to work. I also tried adding [EnableCors("AllowAllOrigins")] to the controller I'm calling.
I've changed the config order based on the recommendation in the comments and identified the property causing the issue:
app.UseIISPlatformHandler();
app.UseCors("AllowAllOrigins");
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.RequireHttpsMetadata = false;
options.Audience = "c8cf662a-ac73-4050-b285-cda90e22992e";
options.Authority = "iwdwk.com";
});
app.UseMvc();
In the code above, the line below seems to be causing the issue:
options.AutomaticAuthenticate = true;
Unfortunately, I need to have that enabled so I can pass the JWT token through for authorization... Unless there is another way to do this?
I guess, the OPTIONS call from your browser are getting rejected by authentication since they may not contain the bearer token. I am not sure but there must be a way to skip authentication for OPTIONS call when calling UseJwtBearerAuthentication. If you wish to confirm this before then try calling your endpoint from postman, since it skips the OPTIONS call and see if you get the access control headers for actual GET/POST call
If the server throws and exception they clear the headers and so you'll only see the CORS error. The way I used to see what was actually going on was to first get the Bearer Token out of one of my failed requests using the Chrome dev tools.
Next I copied that token and pasted into Postman as the value for an Authorization header in a request. When I did that I finally received that nice developer exception page that told me exactly what I was doing wrong. Which was that I was using the ClientId GUID Azure AD gives out instead of the App URI.
So if you are using Azure AD then your JWT options should look like:
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.Authority = "https://login.microsoftonline.com/yourtenant.onmicrosoft.com"
options.Audience = "https://yourtenant.onmicrosoft.com/AppURI"
});
I was having a similar issue. When making a POST/GET and the browser fire a Preflight with OPTIONS request, my angular app wasn't able to understand that was a 401 response. It was returning as 0 - Unknown error.
My problem was the order of my services. All I had to do was in Configure method inside Startup.cs file, change the order. I added first the CORS config and then the JWT middleware and then UseAuthentication
Realize this question is old but it's likely due to the fact that you're performing authentication across CORS with both AllowCredentials=true and AllowAnyOrigin=true. This isn't allowed. Try specifying a specific origin.
Related
I've got an MVC5 Azure web app, which has been working fine with OIDC, using Azure AD as the identity provider.
I'm now trying to put the app behind FrontDoor, but something isn't configured correctly, as the initial request (the one that triggers authenticatation) always redirects to the underlying web app address (app.azurewebsites.net), instead of the FrontDoor (app.azurefd.net). Subsequent requests to the FrontDoor address (that don't need authenticating) work fine.
The web app has an access restriction rule to prevent any access except via the FrontDoor.
The redirect URI configured in the app is set to the FrontDoor address (app.azurefd.net/signin-oidc).
The Azure app registration also has the FrontDoor version (app.azurefd.net/signin-oidc).
The SecurityTokenValidated notification is firing which, if I understand correctly, means that the request from the identity provider back to the redirect URI (app.azurefd.net/signin-oidc) has worked fine, but the final step where it redirects to the URL originally requested is not using the FrontDoor address.
I've tried using FrontDoor Classic and Standard and have tried (with both) having the origin host header match the host name, or being blank. When they match, the behaviour is as described above. Using a blank origin host header is suggested in various places but appears to no-longer work - it now results in a 404.
This GitHub issue describes a very similar problem, but it's using .NET Core, and I'm not sure what the .NET Framework equivalent of the solution is:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto;
});
My startup.cs class looks like this:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = (context) =>
{
_logger.LogDebug<Startup>("Headers:");
foreach (var header in context.Request.Headers)
{
_logger.LogDebug<Startup>($" {header.Key}: {string.Join(" | ", header.Value)}");
}
var name = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;
context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Name, name, string.Empty));
return Task.FromResult(0);
}
}
});
}
I've tried setting the redirect URI using RedirectUri and PostLogoutRedirectUri, but both behave the same.
The FrontDoor origin looks like this:
And the origin group looks like this:
I'm happy to post more config screenshots if that might help, but I'm not sure which bits would be useful.
As far as I can work out, the problem is that the OWIN middleware doesn't use the X-Forwarded-Host from the request, and there doesn't seem to be any option to change this. The .NET Core version makes it easy to configure how headers like this are handled using UseForwardedHeaders (docs), but I couldn't find any equivalent in .NET Framework, so I wrote my own, based on this answer to a similar problem.
public static class UseForwardedHeadersExtension
{
private const string ForwardedHeadersAdded = "ForwardedHeadersAdded";
/// <summary>
/// Checks for the presence of <c>X-Forwarded-Host</c> header, and if present updates the <c>HTTP_HOST</c> header.
/// </summary>
/// <remarks>
/// This extension method is required for running behind FrontDoor. FrontDoor adds the <c>X-Forwarded-Host</c> headers to indicate the host from the original request.
/// </remarks>
public static IAppBuilder UseForwardedHeaders(this IAppBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// no need to add more than one instance of this middleware to the pipeline.
if (app.Properties.ContainsKey(ForwardedHeadersAdded)) return app;
app.Properties[ForwardedHeadersAdded] = true;
app.Use(async (context, next) =>
{
var request = context.Request;
if (request.Headers.ContainsKey("X-Forwarded-Host"))
{
var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
var serverVars = httpContext.Request.ServerVariables;
serverVars["HTTP_HOST"] = request.Headers["X-Forwarded-Host"];
}
await next.Invoke().ConfigureAwait(false);
});
return app;
}
}
This is hugely simplistic compared to the .NET Core implementation in ForwardedHeadersExtensions.cs and ForwardedHeadersMiddleware.cs, but given that access to the app is limited to FrontDoor (it can't be accessed directly) it should be safe, as the header will always be set by FrontDoor. There's a great explanation of the security implications in this blog post, which links to this tweet.
Remove the backend origin host header. Leave it blank
I have two apis written in .NET Core and targeting 4.6.1:
myAuthApi (http://localhost:8496): which verifies credentials and issues tokens to clients. It also has an endpoint /api/values/1 (with an Authorize attribute on this action, used to validate tokens)
myPublicApi(http://localhost:8497): which receives tokens from the client on /api/values/1 (with an Authorize attribute on this action, also used to validate tokens). myPublicApi does not have any tokens endpoint and is meant to be a resource server.
I am using AspNet.Security.OpenIdConnect.Server 1.0.0.
Both APIs are Service.Fabric Stateless Services
I can successfully get the token with the following request format to http://localhost:8496/connect/token
client_id=XX&client_secret=XXX&grant_type=password&username=XXX&password=XXX
When validating the token against myAuthApi (http://localhost:8496/api/values/1) it works. However, when using that same token against myPublicApi(http://localhost:8497/api/values/1) it does not.
In both APIs, in the Startup.cs, I have
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Connect to Redis database.
var redis = ConnectionMultiplexer.Connect(ConnectionHelper.GetRedisConnectionString(Configuration));
services.AddDataProtection()
.PersistKeysToRedis(redis, "DataProtection-Keys")
.ProtectKeysWithCertificate(CertificateHandler.GetX509Certificate2(Configuration));
// Add framework services.
services.AddMvc().AddJsonOptions(opts =>
{
// we set the json serializer to follow camelCaseConventions when
// receiving /replying to JSON requests
opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
// we add authentication for the oAuth middleware to be registered in the DI container
services.AddAuthentication();
}
In myPublicApi I have:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Add a new middleware validating access tokens.
app.UseOAuthValidation(options =>
{
// Automatic authentication must be enabled
// for SignalR to receive the access token.
options.AutomaticAuthenticate = true;
options.Events = new OAuthValidationEvents
{
// Note: for SignalR connections, the default Authorization header does not work,
// because the WebSockets JS API doesn't allow setting custom parameters.
// To work around this limitation, the access token is retrieved from the query string.
OnRetrieveToken = context =>
{
// Note: when the token is missing from the query string,
// context.Token is null and the JWT bearer middleware will
// automatically try to retrieve it from the Authorization header.
context.Token = context.Request.Query["access_token"];
return Task.FromResult(0);
}
};
});
app.UseMvc();
}
In myAuthApi I have:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Add a new middleware validating access tokens.
app.UseOAuthValidation(options =>
{
// Automatic authentication must be enabled
// for SignalR to receive the access token.
options.AutomaticAuthenticate = true;
options.Events = new OAuthValidationEvents
{
// Note: for SignalR connections, the default Authorization header does not work,
// because the WebSockets JS API doesn't allow setting custom parameters.
// To work around this limitation, the access token is retrieved from the query string.
OnRetrieveToken = context =>
{
// Note: when the token is missing from the query string,
// context.Token is null and the JWT bearer middleware will
// automatically try to retrieve it from the Authorization header.
context.Token = context.Request.Query["access_token"];
return Task.FromResult(0);
}
};
});
// Add a new middleware issuing access tokens.
app.UseOpenIdConnectServer(options =>
{
options.Provider = new AuthenticationProvider();
// Enable the logout, token and userinfo endpoints.
options.LogoutEndpointPath = "/connect/logout";
options.TokenEndpointPath = "/connect/token";
options.UserinfoEndpointPath = "/connect/userinfo";
CertificateHandler.SetupCommonAuthServerOptions(options, Configuration);
});
app.UseMvc();
}
As you can see, my data protection provider is storing keys in Redis, and I am protecting keys with a certificate which I am sharing across the 2 applications.
The resource server does not have any authentication provider configured and does not have UseOpenIdConnectServer in startup. In asp.net Web API 2, the token decryption used to be managed across the apps using shared machine keys.
How can I successfully validate the token issued by myAuthApi across all other apps using oAuthValidation?
EDIT, some logs can be seen here:
https://pastebin.com/ACDz1fam
EDIT2 :
After reading the logs thoroughly, I saw that the unprotection of the token was using the same Data Protection Provider, but different purposes:
"Performing unprotect operation to key {4406cfa7-a588-44ba-b73a-e25817d982d9} with purposes ('C:\SfDevCluster\Data\_App\_Node_4\TestMicroServicesType_App22\PublicApiPkg.Code.1.0.1', 'OpenIdConnectServerHandler', 'AccessTokenFormat', 'ASOS')."
"Performing unprotect operation to key {4406cfa7-a588-44ba-b73a-e25817d982d9} with purposes ('C:\SfDevCluster\Data\_App\_Node_3\TestMicroServicesType_App22\AuthApiPkg.Code.1.0.1', 'OpenIdConnectServerHandler', 'AccessTokenFormat', 'ASOS')."
To fix this, #PinpointTownes suggested to setup the data protection provider like so:
var redis = ConnectionMultiplexer.Connect(ConnectionHelper.GetRedisConnectionString(Configuration));
services.AddDataProtection()
// set the application name to a common value in all apps
// to have the same purpose and share the token across apps
.SetApplicationName("MyTestMicroServices")
.PersistKeysToRedis(redis, "DataProtection-Keys")
.ProtectKeysWithCertificate(CertificateHandler.GetX509Certificate2(Configuration));
Call services.AddDataProtection().SetApplicationName("[your application name]") to ensure your two APIs use the same discriminator (used to derive the crypto keys) and it should work.
So I have an MVC6 app that includes an identity server (using ThinkTecture's IdentityServer3) and an MVC6 web services application.
In the web services application I am using this code in Startup:
app.UseOAuthBearerAuthentication(options =>
{
options.Authority = "http://localhost:6418/identity";
options.AutomaticAuthentication = true;
options.Audience = "http://localhost:6418/identity/resources";
});
Then I have a controller with an action that has the Authorize attribute.
I have a JavaScript application that authenticates with the identity server, and then uses the provided JWT token to access the web services action.
This works, and I can only access the action with a valid token.
The problem comes when the JWT has expired. What I'm getting is what appears to be a verbose ASP.NET 500 error page, that returns exception information for the following exception:
System.IdentityModel.Tokens.SecurityTokenExpiredException
IDX10223: Lifetime validation failed. The token is expired.
I am fairly new to OAuth and securing Web APIs in general, so I may be way off base, but a 500 error doesn't seem appropriate to me for an expired token. It's definitely not friendly for a web service client.
Is this the expected behavior, and if not, is there something I need to do to get a more appropriate response?
Edit: this bug was fixed in ASP.NET Core RC2 and the workaround described in this answer is no longer needed.
Note: this workaround won't work on ASP.NET 5 RC1, due to this other bug. You can either migrate to the RC2 nightly builds or create a custom middleware that catches the exceptions thrown by the JWT bearer middleware and returns a 401 response:
app.Use(next => async context => {
try {
await next(context);
}
catch {
// If the headers have already been sent, you can't replace the status code.
// In this case, throw an exception to close the connection.
if (context.Response.HasStarted) {
throw;
}
context.Response.StatusCode = 401;
}
});
Sadly, that's how the JWT/OAuth2 bearer middleware (managed by MSFT) currently works by default, but it should be eventually fixed. You can see this GitHub ticket for more information: https://github.com/aspnet/Security/issues/411
Luckily, you can "easily" work around that by using the AuthenticationFailed notification:
app.UseOAuthBearerAuthentication(options => {
options.Notifications = new OAuthBearerAuthenticationNotifications {
AuthenticationFailed = notification => {
notification.HandleResponse();
return Task.FromResult<object>(null);
}
};
});
I have current API (web api 2) project that has a number of message handlers in use, some run for every request (checking to make sure the request is https and the request is coming from an authorised client) and some that run on specific routes (checking for the existence of a security token).
Now my question is how do I replicate this functionality in MVC 6, my current understanding is that it should be done through the use of middleware but I've not found an example that allows me to inspect the headers of the incoming request and should they not be what they are supposed to be return the appropriate http response code.
Middleware is definitely the right option to solve what you're looking to solve. I wrote a decent explanation about using/writing middlware here: ASP.NET MVC 6 handling errors based on HTTP status code
To specifically answer how to inspect headers here's an example:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
if (!string.Equals(context.Request.Headers["myheader"], "somevalue", StringComparison.Ordinal))
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Invalid headers");
}
else
{
await next();
}
});
}
}
I have an ASP.NET MVC 5 app that authenticates against Azure Active Directory. I wanted to enable SSL on it across the app. and hence leveraged global filters as follows:
public class FilterConfig
{
/// <summary>
/// Registers the global filters.
/// </summary>
/// <param name="filters">The filters.</param>
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new RequireHttpsAttribute());
}
}
After this I also set 'Enable SSL' in the project's properties to true. This gave me the following SSL URL -> https://localhost:34567. I updated the project to have this in its IIS Express path under the 'Web Tab' under Servers in 'Project URL'. However on running the site I run in to the following error:
IDX10311: RequireNonce is 'true' (default) but validationContext.Nonce is null. A nonce cannot be validated. If you don't need to check the nonce, set OpenIdConnectProtocolValidator.RequireNonce to 'false'.
I have auth. enabled on the site. I use Azure Active directory.
The security code is as follows:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri
});
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = audience,
Tenant = tenant,
});
The auth. values are being read from the web.config and are as follows:
<add key="ida:ClientId" value="<some_guid>" />
<add key="ida:Audience" value="https://localhost:34567/" />
<add key="ida:AADInstance" value="https://login.windows.net/{0}" />
<add key="ida:Tenant" value="microsoft.onmicrosoft.com" />
<add key="ida:PostLogoutRedirectUri" value="https://localhost:34567/" />
I tried setting RequireNonce to false as directed in the error message as follows:
ProtocolValidator = new OpenIdConnectProtocolValidator
{
RequireNonce = false
}
But this just resulted in an invalid request error.
Could someone help me understand what the problem is here? Everything worked great until SSL was enabled.
You can ignore exceptions if the error message starts with OICE_20004 or contains IDX10311. Note: do it on your own risk.
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
// Ensure the URI is picked up dynamically from the request;
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.RedirectUri = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + context.Request.Uri.PathAndQuery;
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
if (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311"))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
},
}
From the Azure management portal, check that your application under the corresponding active directory has the same Sign On URL and reply URL.
If they are not same, you will get this error.
This happens when you enable SSL because it changes only the sign on URL to the HTTPS URL while the reply URL remains the same HTTP URL.
Edit:
Read on if you want to know exactly why this is happening,
When you try to access your app using the https URL, it sets a cookie with a unique number(nonce) in your browser and hits Azure AD for authentication. After authentication, the browser has to give access to that cookie. But since the sign on URL and reply URL are different the browser does not recognise your app and does not give access to that cookie and hence the application throws this error.
I can reproduce this error by pressing back button couple of times on my web application, even after successful login.
can you try these 2 things:
in your code below:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = mViewWebSite.ClientId,
Authority = mViewWebSite.Authority,
PostLogoutRedirectUri = mViewWebSite.PostLogoutRedirectUri
});
add protocol validator as on of the authentication options, as what error suggest:
ProtocolValidator = new Microsoft.IdentityModel.Protocols.OpenIdConnectProtocolValidator(){
RequireNonce = false
}
or add notification, by this you can catch this error and redirect it to some error page. I do that to make it graceful. Until Katana people fixes it.
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error.aspx?message=" + context.Exception.Message);
return Task.FromResult(0);
}
},
I manage to work around this problem using following method in the Global.asax file. At least this won't show the exception to the client. I use ELMAH to catch exceptions.
protected void Application_Error(object sender, EventArgs args)
{
var ex = Server.GetLastError();
if (ex.Message.Contains("IDX10311:"))
{
Server.ClearError();
Response.Redirect("https://www.yoursitename.com");
}
Well it would probably be best to look at the katana source code, from that i found the exception type to be OpenIdConnectProtocolInvalidNonceException so i handle it like this.
if (n.Exception is OpenIdConnectProtocolInvalidNonceException &&
n.OwinContext.Authentication.User.Identity.IsAuthenticated)
{
n.SkipToNextMiddleware();
return;
}
I have this exception popup on browsers that cache the pages and users that click the back button after login.
The issue here is simple... took me hours to figure this out.
Since I was testing on my local had no https and to tell you the truth when initially creating my app in Azure AD since i wasnt expecting it to be https during my test I made it plain http (replyUrl's HomePage Url, Logout all that jazz)
Then after doing this i encountered the infinate loop issue a lot of people are getting. so then i decided to mock the cert on my local and yep that got rid of the infinate redirect but then brought another one the "IDX10311: RequireNonce is 'true' " one
Long story short... make your AzureAD App https in all its endpoints. and wallah!
#zb3b answer + #jonmeyer answer:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications()
{
...
AuthenticationFailed = (context) =>
{
if ((context.Exception is OpenIdConnectProtocolInvalidNonceException) &&
(context.OwinContext.Authentication.User.Identity.IsAuthenticated))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
},
...
}
});
Just adding another case I just ran into: the network you connect to may be modifying HTML content.
A customer called with an issue: he could not get past this error. It was a new laptop where he had not logged on before. After about one hour of trying several possible solutions, I decided to check the network he was connected to.
It turns out he was connected to a network in an airport, open and unsecured, and not using a VPN service (lacking some SETA there). I don't know exactly who operated that network or what they were doing, but the Azure AD service must have detected some type of tampering with the nonce.
The moment the user connected to a trusted network, the issue was resolved.