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.
IConfigureNamedOptionsbackwards.Configure(string name, ...needs to testif (name == "something") { apply config } else { nothing }. eg.AddJwtBearer("AuthCode", options => { ... })would end up executing; github.com/dotnet/aspnetcore/blob/… github.com/dotnet/runtime/blob/…