简体   繁体   中英

ASP.NET Core 6 : add multiple authentication schemes with multiple authorization policies along with dependency injection

I need to create a framework for coworkers that allows for multiple authentication schemes and correlating authorization policies (since our IDP has multiple allowed approaches), but those schemes require dependency injection because the IDP information is provided from a cloud-based configuration source. The authentication is done with a JWT Bearer token in the cases that I'm working with so far.

I want to stick to the most current practices to be as current as possible. So it appears that I should be using AddAuthentication , AddJwtBearer , and AddAuthorization on my IServiceCollection .

I am expecting a controller or endpoint can be decorated with the AuthorizeAttribute and it will employ the designated default policy, which will use the designated default authN scheme. If the attribute is given constructor parameters of Policy = <Some Non Default Policy> or AuthenticationSchemes = <Some other scheme> , it will switch to those instead.

First, I established that I needed to use dependency injection, so I'm using IConfigureNamedOptions<JwtBearerOptions> .

So I created the options class like this for auth code

public class AuthCodeJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
{
    private readonly IClaimsTransformation _claimsTransformation;
    private readonly ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;

    public AuthCodeJwtBearerOptions(IOptions<TidV4OAuthSettings> tidV4OAuthOptions,
        IClaimsTransformation claimsTransformation)
    {
        _claimsTransformation = claimsTransformation;

        _configurationManager =
            new ConfigurationManager<OpenIdConnectConfiguration>(tidV4OAuthOptions.Value.WellknownUrl,
                new OpenIdConnectConfigurationRetriever());
    }

    public void Configure(JwtBearerOptions options) => Configure("AuthCode", options);

    public void Configure(string name, JwtBearerOptions options)
    {
        var task = Task.Run(async () => await GetTokenValidationParametersAsync());
        options.TokenValidationParameters = task.Result;
        options.Events = new JwtBearerEvents { OnTokenValidated = OnTokenValidated };
    }

    private async Task<TokenValidationParameters> GetTokenValidationParametersAsync()
    {
        var cancellationToken = new CancellationToken();
        var openIdConnectConfiguration = await _configurationManager.GetConfigurationAsync(cancellationToken);
        return new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = openIdConnectConfiguration?.SigningKeys,
            ValidateAudience = false,
            ValidateIssuer = true,
            ValidIssuer = openIdConnectConfiguration?.Issuer,
            ValidateLifetime = true
        };
    }

    private async Task OnTokenValidated(TokenValidatedContext context)
    {
        if (context.Principal == null)
        {
            return;
        }

        context.Principal = await _claimsTransformation.TransformAsync(context.Principal);
    }
}

And I then repeated this pattern for a class called ClientCredentialsJwtBearerOptions , but I have this variation

public void Configure(JwtBearerOptions options) => Configure("ClientCredentials", options);

I register all of this by setting the default authentication scheme, Adding the JWT Bearers by name with an empty delegate, and then I call to configure the options.

Then I assign policies. I may be wrong, but I don't think the policy creation is the issue, but I'll include the code in case it is a problem.

serviceCollection
            .AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = "AuthCode";
                options.DefaultChallengeScheme = "AuthCode";
            })
            .AddJwtBearer("AuthCode", _ => { })
            .AddJwtBearer("ClientCredentials", _ => { });

        serviceCollection.ConfigureOptions<ClientCredentialsJwtBearerOptions>();
        serviceCollection.ConfigureOptions<AuthCodeJwtBearerOptions>();

        serviceCollection.AddAuthorization(options =>
        {
            var authCodePolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .AddAuthenticationSchemes("AuthCode")
                .Build();

            var clientCredentialsPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .AddAuthenticationSchemes("ClientCredentials")
                .Build();

            var allPolicy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .AddAuthenticationSchemes("AuthCode", "ClientCredentials")
                .Build();

            options.AddPolicy("AuthCodeOnly", authCodePolicy);
            options.AddPolicy("ClientCredentialsOnly", clientCredentialsPolicy);
            options.AddPolicy( "AllPolicies", allPolicy);

            options.DefaultPolicy = authCodePolicy;
        });

I am using constants for the strings here, but I wrote literal strings for ease of reading the example.

When I test this, everything works fine if I leave it exactly like this. And it uses the AuthCodeJwtBearerOptions and the request goes through.

But if I change the Authorize attribute to

[Authorize(Policy = "ClientCredentialsOnly")]

Authentication still uses AuthCodeJwtBearerOptions . I can get it to switch by simply reversing the order of the Configure call

serviceCollection.ConfigureOptions<AuthCodeJwtBearerOptions>();
serviceCollection.ConfigureOptions<ClientCredentialsJwtBearerOptions>();        

Suggesting to me that it's just using the last one to be registered and it is not respecting the "Named" functionality of named configurations.

And if I change the default policy, nothing changes.

I feel like I've got most of the pieces that I need to get this working and I'm just misunderstanding what ConfigureOptions does.

Any help is appreciated. Thank you.

There were two problems with my approach, and I think I have a good resolution now.

First, with the help of Jeremy Lakeman I was made aware of the reversed usage of IConfigureNamedOptions<JwtBearerOptions> 's Configure methods. Instead, you perform a name check for the scheme and if it passes, you configure it.

public void Configure(JwtBearerOptions options)
{
    var task = Task.Run(async () => await GetTokenValidationParametersAsync());
    options.TokenValidationParameters = task.Result;
    options.Events = new JwtBearerEvents { OnTokenValidated = OnTokenValidated };
}

public void Configure(string name, JwtBearerOptions options)
{
    if(!name.Equals("AuthCode"))
    {
        return
    }

    Configure(options);    
}

This gets the options to register correctly. However, the second issue I was having was that if you set a default authentication scheme, it will always run, even when another scheme or policy is requested explicitly

I found, however, that if you do not assign a default authentication scheme and only define a default authentication policy, you can have [Authorize] default to the default policy, but [Authorize("ClientCredentialsOnly")] will perform only the scheme and policy for client credentials.

serviceCollection.AddAuthentication()
     .AddJwtBearer("AuthCode", _ => { })
     .AddJwtBearer("ClientCredentials", _ => { });
serviceCollection.AddAuthorization(options =>
   {
       var authCodePolicy = new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes("AuthCode")
           .Build();
       var clientCredentialsPolicy = new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes("ClientCredentials")
           .Build();
       var allPolicy = new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes("AuthCode", "ClientCredentials")
           .Build();
       options.AddPolicy("AuthCodeOnly", authCodePolicy);
       options.AddPolicy("ClientCredentialsOnly", clientCredentialsPolicy);
       options.AddPolicy( "AllPolicies", allPolicy);
       options.DefaultPolicy = options.GetPolicy("AuthCodeOnly")!;
   });

So, let's sum up.


public class AuthCodeJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
{
    private readonly IClaimsTransformation _claimsTransformation;
    private readonly ConfigurationManager<OpenIdConnectConfiguration> _configurationManager;
    private readonly string _name;

    public AuthCodeJwtBearerOptions(IOptions<TidV4OAuthSettings> tidV4OAuthOptions,
        IClaimsTransformation claimsTransformation)
    {
        _name = "AuthCode";
        _claimsTransformation = claimsTransformation;

        _configurationManager =
            new ConfigurationManager<OpenIdConnectConfiguration>(tidV4OAuthOptions.Value.WellknownUrl,
                new OpenIdConnectConfigurationRetriever());
    }

    public void Configure(JwtBearerOptions options)
    {
        var task = Task.Run(async () => await GetTokenValidationParametersAsync());
        options.TokenValidationParameters = task.Result;
        options.Events = new JwtBearerEvents { OnTokenValidated = OnTokenValidated };
    }

    public void Configure(string name, JwtBearerOptions options)
    {
        if(!name.Equals(_name))
        {
            return;
        } 

        Configure(options);
    }

    private async Task<TokenValidationParameters> GetTokenValidationParametersAsync()
    {
        var cancellationToken = new CancellationToken();
        var openIdConnectConfiguration = await _configurationManager.GetConfigurationAsync(cancellationToken);
        return new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = openIdConnectConfiguration?.SigningKeys,
            ValidateAudience = false,
            ValidateIssuer = true,
            ValidIssuer = openIdConnectConfiguration?.Issuer,
            ValidateLifetime = true
        };
    }

    private async Task OnTokenValidated(TokenValidatedContext context)
    {
        if (context.Principal == null)
        {
            return;
        }

        //whatever you need to do once validated including claims transformation
        context.Principal = await _claimsTransformation.TransformAsync(context.Principal);
    }
}

Repeat the above for other schemes that you want to support, switching the _name , token validation parameters, and event logic as needed. I did this for "ClientCredentials" for now.

Now wire it up in your pipeline

serviceCollection.AddAuthentication()
    .AddJwtBearer("AuthCode", _ => { })
    .AddJwtBearer("ClientCredentials", _ => { });

serviceCollection.ConfigureOptions<ClientCredentialsJwtBearerOptions>();
serviceCollection.ConfigureOptions<AuthCodeJwtBearerOptions>();

serviceCollection.AddAuthorization(options =>
{
    var authCodePolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes("AuthCode")
        .Build();
    var clientCredentialsPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes("ClientCredentials")
        .Build();
    var allPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes("AuthCode", "ClientCredentials")
        .Build();
    options.AddPolicy("AuthCodeOnly", authCodePolicy);
    options.AddPolicy("ClientCredentialsOnly", clientCredentialsPolicy);
    options.AddPolicy( "AllPolicies", allPolicy);
    options.DefaultPolicy = options.GetPolicy("AuthCodeOnly")!;
});

Again, huge thanks to Jeremy for the guidance on the options. And also to Marc for correcting the botch job formatting for my first Stack Overflow post.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM