使用多个JWT Bearer身份验证

169

在ASP.NET Core 2中是否支持多个JWT令牌发行者?我想为外部服务提供API,需要同时使用Firebase和自定义JWT令牌发行者的两个令牌源。在ASP.NET Core中,我可以为Bearer身份验证方案设置JWT身份验证,但只能针对一个Authority进行设置:

  services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = "https://securetoken.google.com/my-firebase-project"
            options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = "my-firebase-project"
                    ValidateAudience = true,
                    ValidAudience = "my-firebase-project"
                    ValidateLifetime = true
                };
        }

我可以有多个发行者和受众,但我无法设置多个授权机构。


2
据我所知,您可以向JWT添加任意数量的属性。因此,没有任何阻止您在JWT中记录两个发行者名称的问题。问题在于,如果每个发行者使用不同的密钥进行签名,那么您的应用程序需要知道两个密钥。 - Tim Biegeleisen
4个回答

401

你完全可以实现你想要的:

services
    .AddAuthentication()
    .AddJwtBearer("Firebase", options =>
    {
        options.Authority = "https://securetoken.google.com/my-firebase-project";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "my-firebase-project",
            ValidateAudience = true,
            ValidAudience = "my-firebase-project",
            ValidateLifetime = true
        };
    })
    .AddJwtBearer("Custom", options =>
    {
        // Configuration for your custom
        // JWT tokens here
    });

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();
    });

让我们来看看你的代码和那个代码之间的区别。

AddAuthentication没有参数

如果您设置了默认的身份验证方案,则在每个请求上,身份验证中间件将尝试运行与默认身份验证方案关联的身份验证处理程序。由于现在有两种可能的身份验证方案,因此运行其中一种方案是没有意义的。

使用另一个重载的AddJwtBearer

每个AddXXX方法添加身份验证都有几个重载:

现在,因为您两次使用了相同的身份验证方法,但身份验证方案必须是唯一的,所以您需要使用第二个重载。

更新默认策略

由于请求不再自动进行身份验证,因此在某些操作上放置[Authorize]属性将导致请求被拒绝并发出HTTP 401

由于这不是我们想要的,因为我们希望授权处理程序有机会对请求进行身份验证,所以我们通过指示应尝试使用FirebaseCustom身份验证方案来对授权系统的默认策略进行更改,以对请求进行身份验证。

这并不会阻止您在某些操作上更加严格;[Authorize]属性具有AuthenticationSchemes属性,允许您覆盖哪些身份验证方案是有效的。

如果您有更复杂的场景,可以利用基于策略的授权。我发现官方文档非常好。

假设某些操作仅适用于由Firebase发行的JWT令牌,并且必须具有特定值的声明;您可以按以下方式执行:

// Authentication code omitted for brevity

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();

        options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase")
            .RequireClaim("role", "admin")
            .Build());
    });

您可以在某些操作上使用[Authorize(Policy = "FirebaseAdministrators")]

最后需要注意的一点是:如果您捕获AuthenticationFailed事件并且使用除第一个AddJwtBearer策略以外的任何内容,您可能会看到IDX10501: Signature validation failed. Unable to match key...。这是由于系统逐个检查每个AddJwtBearer直到找到匹配项。通常可以忽略此错误。

更新 - .net core 6

对于较新版本的.net core,您需要指定默认授权,因此.AddAuthentication()将不起作用。

例如:

// Authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Audience = "https://localhost:5000/";
            options.Authority = "https://localhost:5000/identity/";
        })
        .AddJwtBearer("AzureAD", options =>
        {
            options.Audience = "https://localhost:5000/";
            options.Authority = "https://login.microsoftonline.com/eb971100-7f436/";
        });

// Authorization
builder.Services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        JwtBearerDefaults.AuthenticationScheme,
        "AzureAD");
    defaultAuthorizationPolicyBuilder =
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});

请查看https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0#use-multiple-authentication-schemes以获取更多信息。


12
需要更改来自Firebase或自定义解决方案的标头值吗?例如,将标头从 Authorization: Bearer <token> 更改为 Authorization: Firebase <token>?我尝试了这个解决方案,但出现了错误:“未注册 'Bearer' 方案的任何身份验证处理程序。” - Rush Frisby
7
不需要更改标题。错误信息表明您正在引用不存在的身份验证方案(Bearer)。在我们的示例中,已注册的两个方案是Firebase和Custom,它们是.AddJwtBearer方法调用的第一个参数。 - Mickaël Derriey
10
你好,我正在寻找这个解决方案。不幸的是,我遇到了一个异常:"未指定身份验证方案,并且没有找到默认的挑战方案"。options.DefaultPolicy已经设置好了。有什么想法吗? - terjetyl
32
这是一个非常有帮助的回答,把我在各个地方看到的内容整合在一起了。 - Aron
4
@TylerOhlsen 的说法不正确;虽然在你描述的情况下会使用它,但这不是唯一的情况。如果您没有在端点级别指定授权要求,但在 MVC 控制器和/或操作上使用了空的 [Authorize] 属性,则也会使用它。 - Mickaël Derriey
显示剩余26条评论

12

这是对Mickaël Derriey答案的扩展。

我们的应用有一个自定义的授权要求,我们从内部源解决。我们曾经使用Auth0,但现在正在切换到使用OpenID的Microsoft账户认证。以下是我们ASP.Net Core 2.1 Startup中略作编辑的代码。对于将来的读者,在本文撰写时,这些版本仍然可行。调用方使用从OpenID获得的id_token在传入请求中作为Bearer token使用。希望它能像这个问题和答案对我有所帮助一样,帮助其他想要做身份验证转换的人。

const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);

string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
        .AddJwtBearer(Auth0, options =>
            {
                options.Authority = domain;
                options.Audience = "https://myAuth0Audience.com";
            })
        .AddJwtBearer(MsaOpenId, options =>
            {
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidAudience = "00000000-0000-0000-0000-000000000000",

                    ValidateIssuer = true,
                    ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",

                    ValidateIssuerSigningKey = true,
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
                    RequireSignedTokens = true,
                    ClockSkew = TimeSpan.FromMinutes(10),
                };
                options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
            }
        );

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes( Auth0, MsaOpenId )
        .Build();

    var approvedPolicyBuilder =  new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes(Auth0, MsaOpenId)
           ;

    approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));

    options.AddPolicy("approved", approvedPolicyBuilder.Build());
});

3
您的问题的解决方案可以在以下博客文章中找到:https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme 基本上,解决方案是通过覆盖常规的JWTBearer处理程序,使用自己的通用处理程序来检查JWTBearerConfig中发行人是否与您的令牌中的发行人相同。
该博客建议为每个方案使用单独的处理程序,但这似乎并非必要,一个覆盖HandleAuthenticateAsync方法的通用类JWTAuthenticationHandler似乎就足够了!
代码实现方面,您可以像这样实现启动:
 //Using multiple schemes can cause issues when validating the issuesSigningKey therefore we need to implement seperate handlers for each scheme! => cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
        services.AddAuthentication()
        //Set the authenticationScheme by using the identityServer helper methods (we are using a Bearer token)
        .AddScheme<JwtBearerOptions, JWTAuthenticationHandler>(IdentityServerAuthenticationDefaults.AuthenticationScheme, options =>
        {
            //TO DO Get the origin url's from configuration file, instead of setting all url's here 
            options.Authority = _identityServerSettings.Authority;
            options.Audience = _identityServerSettings.Audience;

            options.Events = new JwtBearerEvents
            {
                OnChallenge = context =>
                {
                    return Task.CompletedTask;
                },
                //When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
                //Cfr => https://dev59.com/5FUL5IYBdhLWcg3w27Jq
                //If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
                //We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
                OnAuthenticationFailed = context =>
                {
                    return Task.CompletedTask;
                },
                OnMessageReceived = context =>
                {
                    return Task.CompletedTask;
                },
                OnForbidden = context =>
                {
                    return Task.CompletedTask;
                },
                OnTokenValidated = context =>
                {
                    return Task.CompletedTask;
                }

            };
        })
        //Set the authentication scheme for the AzureAd integration (we are using a bearer token)
        .AddScheme<JwtBearerOptions, JWTAuthenticationHandler>("AzureAD", "AzureAD", options =>
         {
            options.Audience = _azureAdSettings.Audience;   //ClientId 
            options.Authority = _azureAdSettings.Authority; //"https://login.microsoftonline.com/{tenantId}/v2.0/"

            options.TokenValidationParameters = new TokenValidationParameters
             {
                //Set built in claimTypes => Role
                RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
             };

             options.Events = new JwtBearerEvents
             {
                 OnChallenge = context =>
                 {
                     return Task.CompletedTask;
                 },
                 //When using multiple JwtBearer schemes we can run into "OnAuthenticationFailed" for instance when logging in via IdentityServer the AuthenticationHandler will still check in these events, this can be ignored...
                 //Cfr => https://dev59.com/5FUL5IYBdhLWcg3w27Jq
                 //A final point to note: If you are catching AuthenticationFailed events and using anything but the first AddJwtBearer policy, you may see IDX10501: Signature validation failed.Unable to match key... This is caused by the system checking each AddJwtBearer in turn until it gets a match. The error can usually be ignored.
                 //We managed to fix this issue by adding seperate AuthenticationHandlers for each type of bearer token... cfr: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme
                 OnAuthenticationFailed = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnMessageReceived = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnForbidden = context =>
                 {
                     return Task.CompletedTask;
                 },
                 OnTokenValidated = context =>
                 {
                     return Task.CompletedTask;
                 }
                 
             };
         });
    }

JWTAuthenticationHandlerClass可以看起来像这样

  using Microsoft.AspNetCore.Authentication;
  using Microsoft.AspNetCore.Authentication.JwtBearer;
  using Microsoft.Extensions.Logging;
  using Microsoft.Extensions.Options;
  using System;
  using System.IdentityModel.Tokens.Jwt;
  using System.Text.Encodings.Web;
  using System.Threading.Tasks;

  namespace WebAPI.Auth
  {
    public class JWTAuthenticationHandler: JwtBearerHandler
    {
    public JWTAuthenticationHandler(IOptionsMonitor<JwtBearerOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    { }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        //Fetch OIDC configuration for the IDP we are handling
        var authorityConfig = await this.Options.ConfigurationManager.GetConfigurationAsync(this.Context.RequestAborted);
        //Determine the issuer from the configuration
        var authorityIssuer = authorityConfig.Issuer;

        var jwtToken = this.ReadTokenFromHeader();
        var jwtHandler = new JwtSecurityTokenHandler();

        //Check if we can read the token as a valid JWT, if not let the JwtBearerHandler do it's thing...
        if (jwtHandler.CanReadToken(jwtToken))
        {
            //Read the token and determine if the issuer in config is the same as the one in the token, if this is true we know we want to let the JwtBearerHandler continue, if not we skip and return noResult
            //This way the next IDP configuration will pass here until we find a matching issuer and then we know that is the IDP we are dealing with
            var token = jwtHandler.ReadJwtToken(jwtToken);
            if (string.Equals(token.Issuer, authorityIssuer, StringComparison.OrdinalIgnoreCase))
            {
                return await base.HandleAuthenticateAsync();
            }
            else
            {
                // return NoResult since the issuer in cfg did not match the one in the token, so no need to proceed to tokenValidation
                this.Logger.LogDebug($"Skipping jwt token validation because token issuer was {token.Issuer} but the authority issuer is: {authorityIssuer}");
                return AuthenticateResult.NoResult();
            }
        }

        return await base.HandleAuthenticateAsync();
    }

    //Fetch the bearer token from the authorization header on the request!
    private string ReadTokenFromHeader()
    {
        string token = null;

        string authorization = Request.Headers["Authorization"];

        //If we don't find the authorization header return null
        if (string.IsNullOrEmpty(authorization))
        {
            return null;
        }

        //get the token from the auth header
        if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
        {
            token = authorization.Substring("Bearer ".Length).Trim();
        }

        return token;
    }
}

}


1

在Mickael的回答中缺少的一点是,如果您想使用授权,则需要在Authorize属性中指定方案。

[Authorize(AuthenticationSchemes = "Firebase,Custom", Policy ="FirebaseAdministrators")]

如果未提供AuthenticationSchemes,并且AddAuthentication()没有参数,NetCore将无法进行身份验证,Request.HttpContext.User.Identity.IsAuthenticated将被设置为false。


1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接