OAuth token expiration in MVC6 app - oauth-2.0

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);
}
};
});

Related

MSAL.NET redirect loop when using graphApi in MVC & blazor with multiple instances

I have created a blazor component that aims to simplify managing users and group of an enterprise application in my ASP.NET MVC website. When I run the code locally, everything works just fine. However, when I deploy my code on the dev environment (in AKS) the code only works if I run one replica.
When I use multiple instances and I try to access the page that calls my blazor component, the page ends up in a redirect loop, and finally shows the Microsoft login interface with an error mentioning that the login was not valid.
This is how my code looks like:
# program.cs
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
var cacheOptions = builder.Configuration.GetSection("AzureTableStorageCacheOptions").Get<AzureTableStorageCacheOptions>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
});
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.ConnectionString = cacheOptions.ConnectionString;
options.TableName = cacheOptions.TableName;
options.PartitionKey = cacheOptions.PartitionKey;
options.CreateTableIfNotExists = true;
options.ExpiredItemsDeletionInterval = TimeSpan.FromHours(24);
});
builder.Services.AddSession();
...
# The controller that calls the blazor component
[AuthorizeForScopes(Scopes = new[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" })]
public async Task<IActionResult> UserManagement()
{
string[] scopes = new string[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" };
try
{
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
}
return View();
}
And this is what happens:
If the page loads, I can see this exception in the pod logs:
What am I doing wrong?
The tenant actually needs to provide admin consent to your web API for the scopes you want to use for replicas for the token taken from cache.
Also when AuthorizeForScopes attribute is specified with scopes ,this needs the exact scopes that is required by that api. MsalUiRequiredException gets thrown in case of incorrect scopes for that api and results in a challenge to user.
This error may also occur even when the acquiretokensilent call will not have a valid cookie anymore for authentication in cache .Please check how acquiretokensilent call works from here in msal net acquire token silently | microsoft docs
When valid scopes are given , please make sure the permissions are granted consent by the admin directly from portal or during user login authentication.
Also as a work around try to use use httpContextAccessor to access
token after authentication .
Reference: c# - Error : No account or login hint was passed to the AcquireTokenSilent call - Stack Overflow
So, the culprit was:
#my controller
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
Which we were using initially to reauthenticate the graph api component when we were using InMemoryCache.
There is no need to get the access token again when using DistributedTokenCache, and actually that was causing the token to get saved / invalidated in an infinite loop.
Also, in my blazor component, I had to do use the consent handler to force a login:
private async Task<ServicePrincipal> GetPrincipal(AzureAdConfiguration addConfiguration)
{
try
{
return await GraphClient.ServicePrincipals[addConfiguration.PrincipalId].Request()
.Select("id,appRoles, appId")
.GetAsync();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
throw;
}
}

AuthorizationCodeReceived event not firing

I'm attempting to implement the OpenId Connect middleware in a an ASP.NET MVC 5 (.Net Framework) application.
In my AccountController.cs I send an OpenID Connect sing-in request. I have another OpenId connect middleware implemented which is why I specify that the middleware I want to challenge against is "AlternateIdentityProvider".
public void SignIn()
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
"AlternateIdentityProvider");
}
Upon issuing a challenge against the middleware, the RedirectToIdentityProvider event in Startup.cs fires and I am redirected to the provider for sign in. However, after successfully signing in I am redirected to the specified redirect uri with the state and code parameters added as query parameters i.e. http://localhost:63242/singin-oidc/?state=State&code=AuthorizationCode (parameters removed for brevity), which results in a 404 as no such route exists in my application.
Instead I expected the successful signin to trigger the AuthorizationCodeReceived event where I can implement my additional logic. In fact none of the other events ever trigger.
I have implemented an almost identical solution in ASP.Net Core 2.1 and here I am able to step through the different events as they trigger.
The relevant code of my current Startup.cs is shown below. Note that the OpenId provider throws an error if the inital request include reponse_mode and some telemetry parameters, hence these are removed during the initial RedirectToIdentityProvider event.
Any ideas why the callback from the OpenId provider is not getting picked up in the middleware?
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions("AlternateIdentityProvider")
{
ClientId = { { Client Id } },
ClientSecret = { { Client Secret } },
Scope = OpenIdConnectScope.OpenId,
ResponseType = OpenIdConnectResponseType.Code,
RedirectUri = "http://localhost:63242/singin-oidc",
MetadataAddress = { { Discovery document url } },
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = context =>
{
Debug.WriteLine("Redirecting to identity provider for sign in..");
context.ProtocolMessage.EnableTelemetryParameters = false;
context.ProtocolMessage.ResponseMode = null;
return Task.FromResult(0);
},
AuthorizationCodeReceived = context => {
Debug.WriteLine("Authorization code received..");
return Task.FromResult(0);
},
SecurityTokenReceived = context =>
{
Debug.WriteLine("Token response received..");
return Task.FromResult(0);
},
SecurityTokenValidated = context =>
{
Debug.WriteLine("Token validated..");
return Task.FromResult(0);
},
}
});
I was encountering the same issue. I am trying to plug in Owin into our legacy WebForms app.
For me, I had to do the following:
1) Change the application manifest of the application definition on Azure to set the "oauth2AllowIdTokenImplicitFlow" property to true from false.
Go to the Azure Portal,
Select to Azure Active Directory
Select App Registrations
Select your app.
Click on Manifest
Find the value oauth2AllowIdTokenImplicitFlow and change it's value to true
Click Save
2) In your startup.cs file, change the following:
ResponseType = OpenIdConnectResponseType.Code
to
ResponseType = OpenIdConnectResponseType.CodeIdToken
Once, I did those two things, the SecurityTokenValidated and AuthorizationCodeReceived started firing.
Though, I am not sure this is the right way to go or not. Need to do more reading.
Hope this helps.
Please be aware that the OpenId Connect implementation in .Net Framework only support response_mode=form_post. (See closed GitHub issue)
Since you strip the parameter in the request to the OpenId Connect provider (in your RedirectToIdentityProvider notification), then the provider will default to response_mode=query pr. the specs. (see relation between response_type and response_mode ind the specs.)
So in short the OpenId Connect middleware expects that there will come a HTTP POST (with a form body) and your provider will properly send a HTTP GET (with parameters as query-string).

Exception on cancelled external login

(Don't be afraid of this big description, I just tried to be specific, so that it becomes easier for the answerers)
I'm building a web application using ASP.Net Core 2.1 having external login in it. But internal server error occurred when external (facebook) login is canceled and facebook redirects to the source application.
That means, you clicked on Facebook external login button and then canceled it by clicking on "Not Now" button. Facebook redirects back to your application (https://localhost:port/signin-facebook?...); and then voila -- exception.
An unhandled exception occurred while processing the request.
Exception: access_denied;Description=Permissions error
Unknown location
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
When facebook authentication is getting prepared by the Asp.net Core system from Startup.cs class, 'https://.../signin-facebook' route will be generated automatically by the Facebook authentication provider, as described in the Microsoft docs and Github/aspnet:
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins?view=aspnetcore-2.1&tabs=aspnetcore2x#create-the-app-in-facebook
https://github.com/aspnet/Security/issues/1756#issuecomment-388855389
If I hit "https://localhost:port/signin-facebook" directly without any query-string, it shows this exception: The OAuth state was missing or invalid.
But expected behavior is - it will be redirected to the default login page.
Here's the startup.cs snippet:
services.ConfigureApplicationCookie(options => options.LoginPath = "/Account/LogIn");
services
.AddAuthentication(o => o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
});
services.AddAuthentication()
.AddFacebook(o =>
{
o.AppId = Configuration.GetValue<string>("Facebook:AppId");
o.AppSecret = Configuration.GetValue<string>("Facebook:AppSecret");
});
I configured a custom callbackpath (as descripted in microsoft doc), but same exception.
So..., what's going on? What was the problem? And what's the solution?
FYI, I'm not accessing DB from the application and using default IdentityDbContext with .UseModel() and cookie authentication using HttpContext.SigninAsync. Everything's fine when external login is completed instead of canceling.
According to the Asp.Net Core Github repo, Asp.net Core team is working on it now. Currently developers are using OnRemoteFailure event to handle the exception gracefully and performing the desired action.
Startup.cs:
services.AddAuthentication()
.AddFacebook(o =>
{
o.AppId = Configuration.GetValue<string>("Facebook:AppId");
o.AppSecret = Configuration.GetValue<string>("Facebook:AppSecret");
o.Events.OnRemoteFailure = (context) =>
{
context.Response.Redirect("/account/login");
context.HandleResponse();
return System.Threading.Tasks.Task.CompletedTask;
};
});

Asp.net Core, JWT, and CORS Headers

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.

Implementing Message Handlers in MVC 6

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();
}
});
}
}

Resources