AddJwtBearer()
provides a delegate to configure JwtBearerOptions
, see example. The problem is that there's no way to set the value based on another option.
For example, if you created a custom AuthOptions, which includes basic information like the signing key, audience, issuer, and so on to be configured environment by environment, there's no way to access it in AddJwtBearer()
method.
If you prefer to read the code, here is an implementation.
To address that problem, we leverage IConfigureOptions<T>
interface. There is a caveat to deal with, but in a nutshell, anything registered as IConfigureOptions<T>
in the IoC container will be called when the targeted Option<T>
instance is injected.
For example, if an instance of IOptions<JwtBearerOptions>
is injected, the instance will be initialized by every each of IConfigureOptions<JwtBearerOptions>
in the IoC container.
To leverage that, we could come up with a class like this with 2 things in our mind:
- There is a caveat for this, and it is NOT going to work, yet.
- Since it is a class, injection of other options becomes possible on the constructor.
For example
public class ConfigureJWTBearerOptions : IConfigureOptions<JwtBearerOptions>
{
private readonly AuthOptions _options;
private readonly ILogger _logger;
// Notice IOptions<AuthOptions> has been injected, and could be used by `Configure` later.
public ConfigureJWTBearerOptions(IOptions<AuthOptions> options, ILogger<ConfigureJWTBearerOptions> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public void Configure(JwtBearerOptions options)
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.TokenSigningKey)), // Getting Issuer Signing Key from _options, which is AuthOptions. Same for the issuer and audience.
ValidateIssuer = true,
ValidIssuer = _options.Issuer,
ValidateAudience = true,
ValidAudience = _options.Audience
};
}
}
To register it:
builder.Services.AddTransient<IConfigureOptions<JwtBearerOptions>, ConfigureJWTBearerOptions>();
When an instance of IOptions<JwtBearerOptions>
is instantized from IoC, there is another way, it could be a named option. And the JWTBearerOptions being used leverages the named option. Configuring a named option requires implementing a slightly different interface: IConfigureNamedOptions<T>
.
Here's what the code looks like:
public class ConfigureJWTBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AuthOptions _options;
private readonly ILogger _logger;
public ConfigureJWTBearerOptions(IOptions<AuthOptions> options, ILogger<ConfigureJWTBearerOptions> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public void Configure(JwtBearerOptions options)
{
// This method will NOT be called.
_logger.LogInformation("No name Configure called of {className}", nameof(ConfigureJWTBearerOptions));
Configure(JwtBearerDefaults.AuthenticationScheme, options);
}
public void Configure(string name, JwtBearerOptions options)
{
_logger.LogInformation("{name}.{methodName} is called. Policy name: {policyName}", nameof(ConfigureJWTBearerOptions), nameof(Configure), name);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.TokenSigningKey)),
ValidateIssuer = true,
ValidIssuer = _options.Issuer,
ValidateAudience = true,
ValidAudience = _options.Audience,
NameClaimType = ClaimTypes.Name,
RoleClaimType = ClaimTypes.Role,
};
}
}
Now, you do STILL register it as IConfigureOptions<T>
:
builder.Services.AddTransient<IConfigureOptions<JwtBearerOptions>, ConfigureJWTBearerOptions>();
Alternatively, this will register all the IConfigureOptions by reflection:
builder.Services.ConfigureOptions<ConfigureJWTBearerOptions>();
Notice that IConfigureNamedOptions<T>
inherits IConfiureOption<T>
, and when the IoC go over configurations for options, it always uses IConfigureOptions<T>
.
👍