|
| 1 | +--- |
| 2 | +title: Troubleshooting 401 Unauthorized Errors in ASP.NET Core Web API with Microsoft Entra ID Authentication |
| 3 | +description: Provides guidance for troubleshooting and resolving 401 Unauthorized errors in an ASP.NET Core Web API that uses Microsoft Entra ID authentication. |
| 4 | +ms.date: 04/28/2025 |
| 5 | +ms.author: bachoang |
| 6 | +ms.service: entra-id |
| 7 | +ms.custom: sap:Developing or Registering apps with Microsoft identity platform |
| 8 | +--- |
| 9 | + |
| 10 | +# 401 Unauthorized errors in ASP.NET Core Web API with Microsoft Entra ID |
| 11 | + |
| 12 | +When you call an ASP.NET Core Web API that's secured by using Microsoft Entra ID authentication, you might encounter a "401 Unauthorized" error. This article provides guidance for using `JwtBearerEvents` to capture detailed logs to troubleshoot these errors. |
| 13 | + |
| 14 | +## Symptoms |
| 15 | + |
| 16 | +You use the `[Authorize]` attribute to [secure your ASP.NET Core Web API](/entra/identity-platform/tutorial-web-api-dotnet-core-build-app?tabs=workforce-tenant), as follows: |
| 17 | + |
| 18 | +```csharp |
| 19 | +[Authorize] |
| 20 | +public class MyController : ControllerBase |
| 21 | +{ |
| 22 | + ... |
| 23 | +} |
| 24 | + |
| 25 | +``` |
| 26 | + |
| 27 | +Or |
| 28 | + |
| 29 | +```csharp |
| 30 | + |
| 31 | +public class MyController : ControllerBase |
| 32 | +{ |
| 33 | + [Authorize] |
| 34 | + public ActionResult<string> Get(int id) |
| 35 | + { |
| 36 | + return "value"; |
| 37 | + } |
| 38 | + ... |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +When you call the web API, a "401 Unauthorized" response is returned, but the message contains no error details. |
| 43 | + |
| 44 | +## Cause |
| 45 | + |
| 46 | +The API might return a "401 Unauthorized" response in the following scenarios: |
| 47 | + |
| 48 | +- The request doesn't include a valid "Authorization: Bearer" token header. |
| 49 | +- The token is expired or incorrect: |
| 50 | + - The token is issued for a different resource. |
| 51 | + - The token claims don't meet the application's token validation criteria, as defined in the [JwtBearerOptions.TokenValidationParameters](/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbeareroptions.tokenvalidationparameters) class. |
| 52 | + |
| 53 | +## Solution |
| 54 | + |
| 55 | +To debug and resolve "401 Unauthorized" errors, use the `JwtBearerEvents` callbacks to capture and log detailed error information. Follow these steps to implement a custom error-handling mechanism. |
| 56 | + |
| 57 | +The `JwtBearerEvents` class has the following callback properties (invoked in the following order) that can help you to debug these "401 Access Denied" or "UnAuthorization" issues: |
| 58 | + |
| 59 | +- [`OnMessageRecieved`](/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerevents.onmessagereceived#Microsoft_AspNetCore_Authentication_JwtBearer_JwtBearerEvents_OnMessageReceived) is called first for every request. |
| 60 | +- [`OnAuthenticationFailed`](/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerevents.onauthenticationfailed) is called if the token doesn't pass the application's token validation criteria. |
| 61 | +- [`OnChallenge`](/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer.jwtbearerevents.onchallenge) is called last before a "401" response is returned. |
| 62 | + |
| 63 | +### Step 1: Enable PII logging |
| 64 | + |
| 65 | +By default, personally identifiable information (PII) logging is disabled. Enable it in the **Configure** method of the Startup.cs file for debugging. |
| 66 | + |
| 67 | +> [!Caution] |
| 68 | +> Use 'Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true' only in a development environment for debugging. Do not use it in a production environment. |
| 69 | +
|
| 70 | +```csharp |
| 71 | +public void Configure(IApplicationBuilder app, IHostingEnvironment env) |
| 72 | +{ |
| 73 | + if (env.IsDevelopment()) |
| 74 | + { |
| 75 | + app.UseDeveloperExceptionPage(); |
| 76 | + } |
| 77 | + else |
| 78 | + { |
| 79 | + // The default HSTS value is 30 days. You might want to change this value for production scenarios. See https://aka.ms/aspnetcore-hsts. |
| 80 | + app.UseHsts(); |
| 81 | + } |
| 82 | + // turn on PII logging |
| 83 | + Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true; |
| 84 | + |
| 85 | + app.UseHttpsRedirection(); |
| 86 | + app.UseAuthentication(); |
| 87 | + app.UseMvc(); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +### Step 2: Create a utility method to format exception messages |
| 92 | + |
| 93 | +Add a method to format, and flatten any exception messages for better readability: |
| 94 | + |
| 95 | +```csharp |
| 96 | +public static string FlattenException(Exception exception) |
| 97 | +{ |
| 98 | + var stringBuilder = new StringBuilder(); |
| 99 | + while (exception != null) |
| 100 | + { |
| 101 | + stringBuilder.AppendLine(exception.Message); |
| 102 | + stringBuilder.AppendLine(exception.StackTrace); |
| 103 | + exception = exception.InnerException; |
| 104 | + } |
| 105 | + return stringBuilder.ToString(); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +### Step 3: Implement JwtBearerEvents callbacks |
| 110 | + |
| 111 | +Configure the `JwtBearerEvents` callbacks in the `ConfigureServices` method of *Startup.cs* to handle authentication events and log error details: |
| 112 | + |
| 113 | +```csharp |
| 114 | +public void ConfigureServices(IServiceCollection services) |
| 115 | +{ |
| 116 | +.... |
| 117 | + .AddJwtBearer(options => |
| 118 | + { |
| 119 | + options.Authority = "https://login.microsoftonline.com/<Tenant>.onmicrosoft.com"; |
| 120 | + // if you intend to validate only one audience for the access token, you can use options.Audience instead of |
| 121 | + // using options.TokenValidationParameters which allow for more customization. |
| 122 | + // options.Audience = "10e569bc5-4c43-419e-971b-7c37112adf691"; |
| 123 | +
|
| 124 | + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters |
| 125 | + { |
| 126 | + ValidAudiences = new List<string> { "<Application ID URI>", "10e569bc5-4c43-419e-971b-7c37112adf691" }, |
| 127 | + ValidIssuers = new List<string> { "https://sts.windows.net/<Directory ID>/", "https://sts.windows.net/<Directory ID>/v2.0" } |
| 128 | + }; |
| 129 | + |
| 130 | + options.Events = new JwtBearerEvents |
| 131 | + { |
| 132 | + OnAuthenticationFailed = ctx => |
| 133 | + { |
| 134 | + ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; |
| 135 | + message += "From OnAuthenticationFailed:\n"; |
| 136 | + message += FlattenException(ctx.Exception); |
| 137 | + return Task.CompletedTask; |
| 138 | + }, |
| 139 | + |
| 140 | + OnChallenge = ctx => |
| 141 | + { |
| 142 | + message += "From OnChallenge:\n"; |
| 143 | + ctx.Response.StatusCode = StatusCodes.Status401Unauthorized; |
| 144 | + ctx.Response.ContentType = "text/plain"; |
| 145 | + return ctx.Response.WriteAsync(message); |
| 146 | + }, |
| 147 | + |
| 148 | + OnMessageReceived = ctx => |
| 149 | + { |
| 150 | + message = "From OnMessageReceived:\n"; |
| 151 | + ctx.Request.Headers.TryGetValue("Authorization", out var BearerToken); |
| 152 | + if (BearerToken.Count == 0) |
| 153 | + BearerToken = "no Bearer token sent\n"; |
| 154 | + message += "Authorization Header sent: " + BearerToken + "\n"; |
| 155 | + return Task.CompletedTask; |
| 156 | + }, |
| 157 | +#For completeness, the sample code also implemented the OnTokenValidated property to log the token claims. This method is invoked when authentication is successful |
| 158 | + OnTokenValidated = ctx => |
| 159 | + { |
| 160 | + Debug.WriteLine("token: " + ctx.SecurityToken.ToString()); |
| 161 | + return Task.CompletedTask; |
| 162 | + } |
| 163 | + }; |
| 164 | + }); |
| 165 | +... |
| 166 | +} |
| 167 | +``` |
| 168 | + |
| 169 | +### Sample results |
| 170 | + |
| 171 | +When you implement `JwtBearerEvents` callbacks, if a "401 Unauthorized" error occurs, the response output should include such details as the following example: |
| 172 | + |
| 173 | +```Output |
| 174 | +OnMessageRecieved: |
| 175 | +
|
| 176 | +Authorization Header sent: no Bearer token sent. |
| 177 | +``` |
| 178 | + |
| 179 | +If you use the API development tool to debug the request, you should receive error details, as shown in the following screenshot. |
| 180 | + |
| 181 | +:::image type="content" source="media/401-unauthorized-aspnet-core-web-api/wrong-token.png" alt-text="Screenshot of error details in the API development tool." lightbox="media/401-unauthorized-aspnet-core-web-api/wrong-token.png"::: |
| 182 | + |
| 183 | +[!INCLUDE [Azure Help Support](../../../includes/azure-help-support.md)] |
0 commit comments