ASP.NET Core 6:添加具有多个授权策略的多个身份验证方案以及依赖项注入

r s*_*ton 9 c# oauth-2.0 jwt asp.net-core

我需要为同事创建一个框架,允许多种身份验证方案和关联授权策略(因为我们的 IDP 有多种允许的方法),但这些方案需要依赖项注入,因为 IDP 信息是从基于云的配置源提供的。在我到目前为止所处理的情况下,身份验证是使用 JWT Bearer 令牌完成的。

我想坚持最新的做法,尽可能与时俱进。所以看来我应该在我AddAuthentication的.AddJwtBearerAddAuthorizationIServiceCollection

我期望控制器或端点可以用 来装饰,AuthorizeAttribute并且它将采用指定的默认策略,该策略将使用指定的默认 authN 方案。如果该属性被赋予构造函数参数Policy = <Some Non Default Policy>AuthenticationSchemes = <Some other scheme>,它将切换到这些参数。

首先,我确定我需要使用依赖注入,所以我使用IConfigureNamedOptions<JwtBearerOptions>.

所以我为授权代码创建了这样的选项类

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我在一个名为 的类中重复了这个模式ClientCredentialsJwtBearerOptions,但我有这个变体

public void Configure(JwtBearerOptions options) => Configure("ClientCredentials", options);
Run Code Online (Sandbox Code Playgroud)

我通过设置默认身份验证方案、使用空委托按名称添加 JWT 承载来注册所有这些,然后调用来配置选项。

然后我分配策略。我可能是错的,但我不认为策略创建是问题所在,但我会包含代码,以防出现问题。

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;
        });
Run Code Online (Sandbox Code Playgroud)

我在这里使用字符串常量,但为了便于阅读示例,我编写了文字字符串。

当我测试这个时,如果我完全像这样保留它,一切都会正常。它使用并且AuthCodeJwtBearerOptions请求通过。

但是如果我将Authorize属性更改为

[Authorize(Policy = "ClientCredentialsOnly")]
Run Code Online (Sandbox Code Playgroud)

身份验证仍然使用AuthCodeJwtBearerOptions. Configure我可以通过简单地颠倒调用顺序来切换它

serviceCollection.ConfigureOptions<AuthCodeJwtBearerOptions>();
serviceCollection.ConfigureOptions<ClientCredentialsJwtBearerOptions>();        
Run Code Online (Sandbox Code Playgroud)

向我建议,它只是使用最后一个注册的配置,并且不尊重命名配置的“命名”功能。

如果我更改默认策略,则不会发生任何变化。

我觉得我已经拥有了让这个工作正常运行所需的大部分内容,但我只是误解了它的作用ConfigureOptions

任何帮助表示赞赏。谢谢。

r s*_*ton 6

我的方法有两个问题,我想我现在有一个很好的解决方案。

首先,在Jeremy Lakeman的帮助下,我了解到 的方法的反向IConfigureNamedOptions<JwtBearerOptions>用法Configure。相反,您对方案执行名称检查,如果通过,则对其进行配置。

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);    
}
Run Code Online (Sandbox Code Playgroud)

这将获得正确注册的选项。但是,我遇到的第二个问题是,如果您设置默认身份验证方案,它将始终运行,即使明确请求另一个方案或策略也是如此

但是,我发现,如果您不分配默认身份验证方案而仅定义默认身份验证策略,则可以使用[Authorize]默认策略,但[Authorize("ClientCredentialsOnly")]只会执行客户端凭据的方案和策略。

serviceCollection.AddAuthentication()
     .AddJwtBearer("AuthCode", _ => { })
     .AddJwtBearer("ClientCredentials", _ => { });
Run Code Online (Sandbox Code Playgroud)
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")!;
   });
Run Code Online (Sandbox Code Playgroud)

那么,我们来总结一下。


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);
    }
}
Run Code Online (Sandbox Code Playgroud)

对您想要支持的其他方案重复上述操作,_name根据需要切换 、令牌验证参数和事件逻辑。我现在为“ClientCredentials”执行此操作。

现在将其连接到您的管道中

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")!;
});
Run Code Online (Sandbox Code Playgroud)

再次非常感谢Jeremy对选项的指导。还要感谢Marc,纠正了我第一篇 Stack Overflow 帖子中糟糕的作业格式。