.NET
Handling JWT Authentication with ASP.NET Core TestServer
02 Jun 2023
When writing APIs using ASP.NET Core we can use the excellent Microsoft.AspNetCore.TestHost or Microsoft.AspNetCore.Mvc.Testing packages to write tests which execute the API endpoints themselves in a test server. This allows us to easily write tests which integrate all layers of our API. However there are some challenges with this style of test where authentication is involved.
Microsofts own documentation offers an approach which involves creating a custom AuthenticationHandler<TOptions>
which can
allow us to bypass API authentication in tests. Personally, I'm not a fan. I would much prefer to
write tests for ASP.NET Core APIs in such a way where the authorization policies I have configured for
my JWT bearer authentication are hit as part of the tests.
Fortunately, it's possible to write tests where we can send a JWT to the API and exercise our authorization
policies in the process. To do this we need to reconfigure JwtBearerOptions
to provide a different signature
validator which will allow us to create a JWT in tests and have it accepted by the API under test. At this point,
you can continue reading or just skip to the sample.
First let's assume we have authorization services configured as follows:
public static void AddAuthorizationServices(this IServiceCollection services, IConfiguration configuration)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.Configure<JwtBearerOptions>(configuration.GetSection("JwtBearer"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
options.AddPolicy("Admin", policy => policy.RequireClaim("role", "admin"));
});
}
This configures JWT bearer authentication, sets up a default authorization policy which requires an
authenticated user, and then adds an additional policy which requires the user to be in the admin
role.
We can then set up an API to use the admin policy:
app.MapGet("/people", () => new[]
{
new Person("John", "Doe"),
new Person("Jane", "Doe"),
new Person("John", "Smith"),
new Person("Jane", "Smith")
}).RequireAuthorization("Admin");
If we try to use this API endpoint in tests with the authorization configuration above, we'll get a
a 401 Unauthorized
response. We need to reconfigure JwtBearerOptions
to use a custom signature
validator in our test setup:
The test server setup has been omitted for brevity. See the sample for a full working example.
private static void ConfigureAuthenticationForTests(WebApplicationBuilder builder)
{
// Remove the existing configuration from the API, we don't need that...
builder.Services.RemoveAll<IPostConfigureOptions<JwtBearerOptions>>();
// Reconfigure JwtBearerOptions to use a custom token validator
builder.Services.PostConfigure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
SignatureValidator = (token, _) => new JwtSecurityToken(token), ValidateAudience = false, ValidateIssuer = false
};
});
}
Here we skip validation of the token audience and issuer and simply create a new JwtSecurityToken
instance
from the provided token string. The framework will then be able to use this like our regular authentication
and create a ClaimsPrincipal
containing the ClaimsIdentity
for our test "user" represented by the JWT.
Now we can create a JWT signed by a random key in the tests:
private static string CreateTestJwt(string role)
{
var securityTokenDescriptor = new SecurityTokenDescriptor
{
NotBefore = DateTime.UtcNow,
Expires = DateTime.UtcNow.AddMinutes(1),
SigningCredentials = new SigningCredentials(new RsaSecurityKey(RSA.Create()), SecurityAlgorithms.RsaSha512),
Subject = new ClaimsIdentity(new List<Claim>
{
new("name", "Some User"), new("role", role)
})
};
var securityTokenHandler = new JwtSecurityTokenHandler();
var token = securityTokenHandler.CreateToken(securityTokenDescriptor);
var encodedAccessToken = securityTokenHandler.WriteToken(token);
return encodedAccessToken;
}
and use it in our tests to call the authenticated API endpoint:
[Fact]
public async Task User_is_authenticated_by_test_token()
{
var httpClient = await CreateTestServer();
var jwt = CreateTestJwt("admin");
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/people")
{
Headers =
{
Authorization = new AuthenticationHeaderValue(JwtBearerDefaults.AuthenticationScheme, jwt)
}
});
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
Now we have a solution which allows us to use the test server to exercise our API logic and the authorization policies associated with it. This is a much better approach than mocking authentication and authorization entirely with a new authorization handler.