I am getting an unexpected Unauthorized response from an Api when using JWT in Blazor WebAssembly. Note, I am not trying to secure anything on the WebAssembly client; just the API endpoint. I have deliberately left out expiry validation.
Server
appsettings.json
{
"JwtSecurity": {
"Key": "RANDOM_KEY_MUST_NOT_BE_SHARED",
"Issuer": "https://localhost",
"Audience": "https://localhost",
"ExpiryDays": 1
}
}
Program.cs
// Service registration
builder.Services
.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["JwtSecurity:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["JwtSecurity:Audience"],
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSecurity:Key"])),
RequireExpirationTime = false,
ValidateLifetime = false
};
});
// Configure the HTTP request pipeline.
// SignalR Compression
app.UseResponseCompression();
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
// Logs the received token
app.UseJwtTokenHandler();
//explicitly only use blazor when the path doesn't start with api
app.MapWhen(ctx => !ctx.Request.Path.StartsWithSegments("/api"), blazor =>
{
blazor.UseBlazorFrameworkFiles();
blazor.UseStaticFiles();
blazor.UseRouting();
blazor.UseEndpoints(endpoints =>
{
endpoints.MapHub<Cosmos.App.Server.Hubs.TillSiteHub>("/tradingsessionhub");
endpoints.MapFallbackToFile("index.html");
});
});
//explicitly map api endpoints only when path starts with api
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
api.UseStaticFiles();
api.UseRequestLogging();
api.UseRouting();
api.UseAuthentication();
api.UseAuthorization();
// HAVE ALSO TRIED
// api.UseAuthentication();
// api.UseRouting();
// api.UseAuthorization();
api.UseErrorHandling();
api.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
app.Run();
- This link suggests UseRouting should come before UseAuthentication and UseAuthorisation.
- This link suggests UseRouting should come between them.
Have tried both to no avail.
Token Generation on Login
Helper Class
public class JwtHelper
{
public static JwtSecurityToken GetJwtToken(
string username,
string signingKey,
string issuer,
string audience,
TimeSpan expiration,
Claim[] additionalClaims = null)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub,username),
// this guarantees the token is unique
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
if (additionalClaims is object)
{
var claimList = new List<Claim>(claims);
claimList.AddRange(additionalClaims);
claims = claimList.ToArray();
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
return new JwtSecurityToken(
issuer: issuer,
audience: audience,
expires: DateTime.UtcNow.Add(expiration),
claims: claims,
signingCredentials: creds
);
}
}
Controller Method for Login
Guid userGid = await loginManager.LoginAsync(request.Email!, request.Password!);
if (userGid == default)
{
return base.NotFound();
}
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, userGid.ToString()));
claims.Add(new Claim(ClaimTypes.Name, userGid.ToString()));
string key = configuration["JwtSecurity:Key"];
string issuer = configuration["JwtSecurity:Issuer"];
string audience = configuration["JwtSecurity:Audience"];
string expiryDays = configuration["JwtSecurity:ExpiryDays"];
TimeSpan expiry = TimeSpan.FromDays(Convert.ToInt32(expiryDays));
var token = JwtHelper.GetJwtToken(
userGid.ToString(),
key,
issuer,
audience,
expiry,
claims.ToArray());
LoginResponse response = new(new JwtSecurityTokenHandler().WriteToken(token));
return base.Ok(response);
The token string response is stored in Local Storage.
Client
Adding header to Http Client
// GetTokenAsync retrieve the string from Local Storage
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue(
"bearer",
(await _accessControlStateManager.GetTokenAsync())!.Write());
Server Logging of Received Token by middleware
public class JwtTokenHandlerMiddleware
{
readonly RequestDelegate _next;
readonly ILogger _logger;
public JwtTokenHandlerMiddleware(
RequestDelegate next,
ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory.CreateLogger(typeof(JwtTokenHandlerMiddleware).FullName!);
}
public async Task Invoke(HttpContext context)
{
JwtSecurityToken? jwt = context.GetJwtTokenFromAuthorizationHeader();
if (jwt != null)
{
_logger.LogInformation("Request received with token: {uri}", context.Request.GetDisplayUrl());
_logger.LogInformation("Token: {token}", jwt.Write());
}
else
{
_logger.LogInformation("Request received from ANONYMOUS: {uri}", context.Request.GetDisplayUrl());
}
await _next(context);
}
}
public static JwtSecurityToken? GetJwtTokenFromAuthorizationHeader(this HttpContext httpContext)
{
string text = httpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
return new JwtSecurityTokenHandler().ReadJwtToken(text);
}
- I have confirmed from the logs that the JWT is being received
- Using jwt.io I can confirm that the token logged in the request middleware can be read
jwt.io output
Given all of the above, it seems that:
- I am generating JWT correcly.
- JWT is being stored and retrieved correctly from Local Storage
- JWT is being received in the Authorization header in the request
- The controller is secured at Controller level using Authorize attribute (no roles mentioned)
But still I get Unauthorized.
Any advice?
Attempts to Isolate
Disable validation parameters
Tried:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
RequireExpirationTime = false,
ValidateLifetime = false
};
And added simple test controller:
using Microsoft.AspNetCore.Authorization;
namespace Cosmos.App.Server.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TestController : ControllerBase
{
[HttpGet]
[Route("test")]
public async Task<IActionResult> TestAsync()
{
await Task.Delay(1);
return Ok("Hello");
}
}
But same issue.
