Skip to content

Instantly share code, notes, and snippets.

@xiaomi7732
Last active May 23, 2024 22:43
Show Gist options
  • Save xiaomi7732/20ff2ad11b085a851759d3835b95c8d7 to your computer and use it in GitHub Desktop.
Save xiaomi7732/20ff2ad11b085a851759d3835b95c8d7 to your computer and use it in GitHub Desktop.
Configure JWT Bearer Options based on other options

Configure JWT Bearer Options based on another option

The problem

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.

Solution with a caveat (See next section for a solution that works)

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:

  1. There is a caveat for this, and it is NOT going to work, yet.
  2. 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>();

The caveat and the solution

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>.

@yogrr
Copy link

yogrr commented Aug 23, 2023

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment