I had two specific desires of my Jwt secrets used for signing. They are provided via a secrets.json file created/managed on deployed servers and integrated into the IConfiguration workflow.
- I wanted Jwt secrets updatable in the configuration file and have updates applied to my Jwt verification process without the need to restart the application(s).
- I wanted the ability to have some FastEndpoints endpoints to support a secondary/alternate Jwt secret for signing verification.
The basic flow is the following.
- Add Jwt support via FastEndpoints
Services.AddAuthenticationJwtBearerextenion method (if not using FastEndpoints, look at this helper method to see what manually needs to be done, but mostly it is just call toAddAuthenticationandAddJwtBearer(with appropriate settings)). - Add a
IConfigureOptions<JwtBearerOptions>singleton to the service collection and provide aIssuerSigningKeyResolverdelegate. (Note: this needed to be anIConfigureOptions<JwtBearerOptions>instead of simply assigning this same delegate during theServices.AddAuthenticationJwtBearerconfiguration action because I needed some dependency injection to support reading myIConfiguration).
References:
- Updated Secrets
- This aspnetcore issue describes how I ended up creating the
UpdatableJwtBearerOptionsclass. - My only concern, simply because I'm mostly a novice in DI, is my use of
GetRequiredServicein myIConfigureOptions<JwtBearerOptions>option. I've read callingGetRequiredServiceis an anti-pattern, but this was the only way to get ahold of dependencies I needed and hopefully my use case is not within the anti-pattern range.
- This aspnetcore issue describes how I ended up creating the
- Alternate Jwt
- Originally, I was going to manually call
AddJwtBearerfor primary and secondary signing keys, and then follow FastEndpoints Combined Authentication Scheme pattern, but when I saw that I already had ability to swap to an alternate via myIssuerSigningKeyResolverI used that instead (which allowed me to continue to use theAddAuthenticationJwtBearerconvenience method). - The original FastEndpoints discussion that provided me the information to attempt the multiple
AddJwtBearerpattern.
- Originally, I was going to manually call
Implementation:
Program.cs
// Define the IConfiguration 'key' name to use when request starts with one of the provided paths
var jwtTypes = new Dictionary<string, string[]>
{
{ "xDS", new [] { ApiEndpoints.VersionBase } }
};
var alternateJwtTypes = new Dictionary<string, string[]>
{
{ "ExcelAddIn", new [] { ApiEndpoints.Single.Base, ApiEndpoints.GlobalTables.Update } }
};
builder.Services.AddAuthenticationJwtBearer(
signingOptions: o =>
{
// Need to assign key to non null so AddAuthenticationJwtBearer adds a signing key
o.SigningKey = "secretUpdatedViaUpdatableJwtBearerOptions";
o.SigningStyle = JWTBearer.TokenSigningStyle.Symmetric;
},
bearerOptions: o =>
{
// Configure bearer options as you see fit...
// https://discord.com/channels/933662816458645504/1200521676912332861/1200648920435544144
// https://github.com/FastEndpoints/FastEndpoints/issues/526
// PR explaining reason FastEndpoints changing functionality to set to false, but I don't use FastEndpoint's `JWTBearer.CreateToken` so had a mismatch after update
o.MapInboundClaims = true;
} );
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>>( provider =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
var httpContextAccessor = provider.GetRequiredService<IHttpContextAccessor>();
var options = new UpdatableJwtBearerOptions( configuration, httpContextAccessor, jwtTypes, alternateJwtTypes );
return options;
} );UpdatableJwtBearerOptions.cs
public class UpdatableJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly IConfiguration configuration;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly Dictionary<string, string[]> jwtTypes;
private readonly Dictionary<string, string[]>? alternateJwtTypes;
public UpdatableJwtBearerOptions( IConfiguration configuration, IHttpContextAccessor httpContextAccessor, Dictionary<string, string[]> jwtTypes, Dictionary<string, string[]>? alternateJwtTypes = null )
{
this.configuration = configuration;
this.httpContextAccessor = httpContextAccessor;
this.jwtTypes = jwtTypes;
this.alternateJwtTypes = alternateJwtTypes;
}
public void Configure( string? name, JwtBearerOptions options )
{
options.TokenValidationParameters.IssuerSigningKeyResolver = ( token, securityToken, kid, validationParameters ) =>
{
var ctx = httpContextAccessor.HttpContext!;
var path = ctx.Request.Path;
var jwtType = jwtTypes.Count == 1
? jwtTypes.Keys.First() // If only one type mapping, that is global
: jwtTypes.Keys.FirstOrDefault( k => jwtTypes[ k ].Any( p => path!.StartsWithSegments( p ) ) );
if ( ( alternateJwtTypes?.Any() ?? false ) && ctx.Request.Headers.TryGetValue( "x-kat-alternate-jwt", out var h ) && string.Compare( h.FirstOrDefault(), "true", true ) == 0 )
{
jwtType = alternateJwtTypes.Keys.FirstOrDefault( k => alternateJwtTypes[ k ].Any( p => path!.StartsWithSegments( p ) ) );
}
validationParameters.ValidIssuer =
configuration.GetValue<string>( $"TheKeep:Jwt:{jwtType}:Issuer" ) ??
configuration.GetValue<string>( $"MyKeep:Jwt:{jwtType}:Issuer" );
var secret =
configuration.GetValue<string>( $"TheKeep:Jwt:{jwtType}:Secret" ) ??
configuration.GetValue<string>( $"MyKeep:Jwt:{jwtType}:Secret" )!;
return new List<SecurityKey>()
{
new SymmetricSecurityKey( Encoding.ASCII.GetBytes( secret ) )
};
};
}
[ExcludeFromCodeCoverage( Justification = "Interface method not used/supported" )]
public void Configure( JwtBearerOptions options ) => throw new NotImplementedException();
}