Resolve JWT Signing Key with DI in ASP.NET Core JWT Bearer
Learn how to configure ASP.NET Core JWT Bearer authentication to resolve issuer signing keys dynamically from ISigningKeyRepository using dependency injection and IssuerSigningKeyResolver at runtime.
How to resolve issuer signing key using dependency injection from ISigningKeyRepository in AddJwtBearer configuration in ASP.NET Core?
I want to authenticate users with JWT tokens using an ISigningKeyRepository that handles rotating and creating signing keys in the database. When registering JWT authentication in IServiceCollection with this code:
.AddJwtBearer("UserJwtAuthentication", options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuer = "test",
ValidAudience = "test",
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
};
});
I need to pass the key via IssuerSigningKeyResolver, but the key is resolved using ISigningKeyRepository, which cannot be accessed directly in the IServiceCollection configuration context.
Is there a way to configure JWT Bearer authentication to resolve the signing key from ISigningKeyRepository at runtime using dependency injection?
Yes, you can resolve the issuer signing key dynamically from your ISigningKeyRepository in ASP.NET Core JWT Bearer authentication by setting a custom IssuerSigningKeyResolver delegate in the AddJwtBearer options. This resolver executes at runtime during each token validation, giving you access to the current HttpContext and its RequestServices for dependency injection—no need to hack the startup configuration lambda. Just register your repository as scoped, and pull it from the validation context to fetch the key by kid from the JWT header.
Contents
- Why Direct DI Fails in AddJwtBearer
- The Power of IssuerSigningKeyResolver
- Implementing Key Resolution with DI
- Registering Services and Middleware
- Alternatives for Static or Hybrid Keys
- Security Tips and Pitfalls
- Sources
- Conclusion
Why Direct DI Fails in AddJwtBearer
Ever tried grabbing a service like ISigningKeyRepository inside the AddJwtBearer lambda? It won’t work. That configuration runs during IServiceCollection setup, before the container is built and scoped services like your repo are available. Building a temporary ServiceProvider with services.BuildServiceProvider() might seem tempting—some Stack Overflow answers suggest it for quick key loads—but it creates duplicate singletons and skips proper lifetimes.
Worse, for rotating keys stored in a database, you need runtime access anyway. Static keys? Fine for dev. But production with key rotation demands something dynamic. That’s where JWT signing key resolvers shine. They kick in per request, post-container build.
The Power of IssuerSigningKeyResolver
The IssuerSigningKeyResolver is your runtime hero for JWT Bearer validation. It’s a delegate invoked by JwtSecurityTokenHandler every time a token hits your app. Signature:
Func<string, SecurityToken, TokenValidationParameters, ValidationContext, ResolveSigningKeyResult?>
It gets the raw token, parsed SecurityToken, validation params, and crucially—a ValidationContext with HttpContextAccessor-like access to services. No more config-time struggles.
According to deep dives into the handler’s source, if unset, it falls back to metadata discovery. But set it custom, and you control key lookup by kid (key ID) or x5t thumbprint from the JWT header. Perfect for your repo pulling from DB.
Why does this matter? Token validation happens in middleware, fully scoped. Inject away.
Implementing Key Resolution with DI
Here’s the money shot. In Program.cs or Startup.ConfigureServices:
builder.Services.AddScoped<ISigningKeyRepository, SigningKeyRepository>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("UserJwtAuthentication", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "test",
ValidAudience = "test",
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
// No IssuerSigningKey here—resolver handles it
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
// Optional: Post-validation logic
return Task.CompletedTask;
}
};
// The resolver magic
options.TokenValidationParameters.IssuerSigningKeyResolver =
(tokenString, securityToken, validationParameters, validationContext) =>
{
// DI via RequestServices—scoped, fresh per request
var repo = validationContext.HttpContext.RequestServices
.GetRequiredService<ISigningKeyRepository>();
if (securityToken is JwtSecurityToken jwtToken)
{
var kid = jwtToken.Header.Kid; // Or x5t
var key = repo.GetSigningKeyAsync(kid).GetAwaiter().GetResult(); // Sync for simplicity; async possible with custom handler
if (key != null)
{
return new[] { new SymmetricSecurityKey(key.KeyBytes) }; // Or RsaSecurityKey
}
}
return null; // Invalid key → fail auth
};
});
Boom. Your repo resolves the JWT signing key by kid at validation time. GetAwaiter().GetResult() works for sync delegate; for full async, extend JwtBearerHandler. Test with a rotating key—issue a token with new kid, watch it validate seamlessly.
But what if no kid matches? Resolver returns null, auth fails. Graceful.
Registering Services and Middleware
Don’t forget the basics. Your ISigningKeyRepository must be scoped (per-request lifetime) to hit the DB safely.
// Program.cs (.NET 6+)
builder.Services.AddScoped<ISigningKeyRepository, YourRepo>();
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
For minimal APIs or controllers, that’s it. In controllers, [Authorize(AuthenticationSchemes = "UserJwtAuthentication")] triggers it.
Pro tip: Cache resolved keys in repo with IMemoryCache to dodge DB thrash. services.AddMemoryCache(); then cache.GetOrCreate(keyId, entry => LoadFromDbAsync(keyId));.
Alternatives for Static or Hybrid Keys
Resolver too heavy? For fixed keys, use IOptions<JwtBearerOptions>.Configure:
services.AddSingleton<ISigningKeyRepository>(...); // Singleton for static
services.AddOptions<JwtBearerOptions>("UserJwtAuthentication")
.Configure<ISigningKeyRepository>((options, repo) =>
{
var key = repo.GetCurrentKey(); // Startup-time fetch
options.TokenValidationParameters.IssuerSigningKey = key;
});
Or IPostConfigureOptions<JwtBearerOptions> for tweaks post-registration. GitHub issues from the ASP.NET team highlight these for edge cases, but resolver wins for dynamics.
Hybrid? Resolver first, fallback to static. Flexibility rules.
Security Tips and Pitfalls
Key rotation exposes risks. Always validate kid against allowlist. Cache? Set sliding expiration matching rotation interval. DB perf? Async all the way, or you’ll block middleware.
Pitfalls: Sync-over-async deadlocks—use .Result sparingly. No HttpContext outside requests? Resolver skips gracefully. Logs? Hook OnAuthenticationFailed.
Stuck on invalid signatures? Check clock skew (ClockSkew = TimeSpan.Zero), audience mismatches. Tools like jwt.io decode headers fast.
Sources
- How Do ASP.NET Core Services Validate JWT Signature Signed by AAD? — Details IssuerSigningKeyResolver mechanics and invocation: https://zhiliaxu.github.io/how-do-aspnet-core-services-validate-jwt-signature-signed-by-aad.html
- Better way to get the Json Web Key in AddJwtBearer — Official discussion on DI access limitations and resolver proposals: https://github.com/dotnet/aspnetcore/issues/21491
- Jwt Bearer and dependency injection — Code example of early ServiceProvider pitfalls in JWT config: https://stackoverflow.com/questions/61186836/jwt-bearer-and-dependency-injection
- ASP.NET Core 3.1 JWT signature invalid when using AddJwtBearer() — IOptions.Configure pattern for injecting keys via DI: https://stackoverflow.com/questions/60434420/asp-net-core-3-1-jwt-signature-invalid-when-using-addjwtbearer
- Using dependency injection to replace JWT Bearer Options — PostConfigureOptions for runtime JWT options tweaks: https://stackoverflow.com/questions/51586562/using-dependency-injection-to-replace-jwt-bearer-options-in-asp-net-core
Conclusion
Configuring ASP.NET Core JWT Bearer to pull signing keys from ISigningKeyRepository boils down to IssuerSigningKeyResolver—runtime DI via validationContext.HttpContext.RequestServices. It’s secure, scalable for rotation, and sidesteps startup hacks. Start with the code above, add caching, and you’re golden. Questions on async tweaks or AAD hybrids? The resolver scales there too.