There is plenty of content on the internet that shows you how to use ASP.NET Core Authentication pipeline to protect a Web API with Azure Active Directory. This is all working great except when your Web API returns a 401 Unauthorized error without providing much information. You can end up spending a lot of time, pulling your hair out trying to figure out what can go wrong even though you (or at least believe) have followed every step of the documentation, implemented all the best practices and guidelines. Hopefully, this blog can help alleviate that pain for you. I will build the content here on top of my previous blog post which discusses how you can protect your Web API with Azure Active Directory.
Let’s dig in…
When you use the [Authorize] attribute to protect your web API controller as followed
[Authorize] public class MyController : ControllerBase { ... }
or to protect an action in the controller
public class MyController : ControllerBase { [Authorize] public ActionResult<string> Get(int id) { return "value"; } ... }
It expects the request has a valid Bearer token presented in the “Authorization: Bearer ey…” HTTP Request header. The controller returns a 401 Unauthorized response when the request either does not have an “Authorization Bearer token” header or the request contains an invalid Bearer token (the token is expired, the token is for a different resource, or the token’s claims do not satisfy at least one of the application’s token validation criteria as defined in the JwtBearerOptions’s TokenValidationParameters class).
The JwtBearerEvents class has the following callback properties (invoked in the following order) that can help us debug these 401 Access Denied or UnAuthorization issues:
- OnMessageRecieved – gets called first for every request
- OnAuthenticationFailed – gets called when the token does not pass the application’s token validation criteria.
- OnChallenge – this gets called last before a 401 response is returned
Note: In some scenarios, the OnAuthenticationFailed method may not be invoked, for example: the request does not have any bearer token or it contains a malformed token (invalid jwt token format)
We will use the above callbacks to build our own custom error message to return to the client.
Note: the sample code in this blog is only meant for Proof of Concept and can review PII information only for debugging local development effort so you should be cautious not to use in production environment.
Implement the following code:
- Enable PII logging (it’s off by default) in the Configure method in Startup.cs file:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } // turn on PII logging Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; app.UseHttpsRedirection(); app.UseAuthentication(); app.UseMvc(); }
- Create a method called FlattenException to format our Exception Message
public static string FlattenException(Exception exception) { var stringBuilder = new StringBuilder(); while (exception != null) { stringBuilder.AppendLine(exception.Message); stringBuilder.AppendLine(exception.StackTrace); exception = exception.InnerException; } return stringBuilder.ToString(); }
- Implement the above callbacks in the ConfigureServices method in Startup.cs file
public void ConfigureServices(IServiceCollection services) { .... .AddJwtBearer(options => { options.Authority = "https://login.microsoftonline.com/<Tenant>.onmicrosoft.com"; // if you intend to validate only one audience for the access token, you can use options.Audience instead of // using options.TokenValidationParameters which allow for more customization. // options.Audience = "10e569bc5-4c43-419e-971b-7c37112adf691"; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidAudiences = new List<string> { "<Application ID URI>", "10e569bc5-4c43-419e-971b-7c37112adf691" }, ValidIssuers = new List<string> { "https://sts.windows.net/<Directory ID>/", "https://sts.windows.net/<Directory ID>/v2.0" } }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = ctx => { ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; message += "From OnAuthenticationFailed:\n"; message += FlattenException(ctx.Exception); return Task.CompletedTask; }, OnChallenge = ctx => { message += "From OnChallenge:\n"; ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; ctx.Response.ContentType = "text/plain"; return ctx.Response.WriteAsync(message); }, OnMessageReceived = ctx => { message = "From OnMessageReceived:\n"; ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken); if (BearerToken.Count == 0) BearerToken = "no Bearer token sent\n"; message += "Authorization Header sent: " + BearerToken + "\n"; return Task.CompletedTask; }, OnTokenValidated = ctx => { Debug.WriteLine("token: " + ctx.SecurityToken.ToString()); return Task.CompletedTask; } }; }); ... }
For completeness, I have also implemented the OnTokenValidated property to print out the token claims. This method is invoked when the authentication is successful.
Here is my entire Startup.cs file
[gist id=”90b646e2fedb0a446522d5e0076dddf7″ file=”Startup.cs”]
The result…
The above implementation should result in 401 Error message with some output as followed
or the following
Hi Bac,
I’ve read over quite a few Azure B2C tutorials that talk about implementing JWT with Xamarin.Forms. I’ve even tried to follow the advice in this blog post, but I’m still getting a 401 authorization error, and I can’t figure out why.
I posted a question on Stackoverflow, but it doesn’t seem to be getting much traction:
https://stackoverflow.com/questions/60595397/401-unauthorized-with-azure-b2c-on-xamarin-forms
Do you have any advice or guidance that could point me in the right direction?
Thanks, with your help I was able to discover that the authority and audience were not passed correctly. It took me a while to figure this out. Halelujah!!!
I commented out the request status code change, this was giving errors. It helped me to look at the token that was passed from the client side. Thanks.
Thanks Mark. Glad to hear the content is helpful 🙂
This was so helpful, thank you.