【问题标题】:Use multiple JWT Bearer Authentication使用多个 JWT 承载认证
【发布时间】:2018-09-16 13:49:21
【问题描述】:

是否可以在 ASP.NET Core 2 中支持多个 JWT 令牌颁发者? 我想为外部服务提供 API,我需要使用两个 JWT 令牌来源 - Firebase 和自定义 JWT 令牌颁发者。在 ASP.NET 核心中,我可以为 Bearer 身份验证方案设置 JWT 身份验证,但仅限于一个权限:

  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
                };
        }

我可以有多个发布者和受众,但我不能设置多个权威。

【问题讨论】:

  • AFAIK 您可以向 JWT 添加任意数量的属性。因此,没有什么能阻止您在 JWT 中记录两个颁发者名称。问题在于,如果每个颁发者使用不同的密钥进行签名,您的应用程序将需要知道这两个密钥。

标签: c# firebase-authentication asp.net-core-mvc jwt asp.net-core-2.0


【解决方案1】:

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

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 方法都有几个重载:

  • 使用与身份验证方法关联的默认身份验证方案,您可以see here for cookies authentication
  • 你通过的地方,除了配置options,认证方案的名字,如@9​​87654322@

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

更新默认策略

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

因为这不是我们想要的,因为我们想让身份验证处理程序有机会对请求进行身份验证,所以我们通过指示 FirebaseCustom 身份验证方案应该是 尝试验证请求。

这并不妨碍您对某些操作更加严格; [Authorize] 属性有一个 AuthenticationSchemes 属性,允许您覆盖哪些身份验证方案是有效的。

如果你有更复杂的场景,你可以使用policy-based authorization。我觉得官方文档很棒。

假设某些操作仅适用于 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 直到它得到一场比赛。该错误通常可以忽略。

【讨论】:

  • 这是否需要从 firebase 或自定义解决方案更改标头值? IE。而不是Authorization : Bearer <token>,例如标题是Authorization : Firebase <token>?尝试此解决方案时,我收到错误消息:“没有为方案 'Bearer' 注册身份验证处理程序。”
  • 不,标题不需要更改。该错误消息表明您指的是不存在的身份验证方案(承载)。在我们的示例中,两个已注册的方案是 Firebase 和 Custom,它们是 .AddJwtBearer 方法调用的第一个参数。
  • 嗨。正在寻找这个解决方案。不幸的是,我收到“未指定身份验证方案,并且未找到 DefaultChallengeScheme”异常。 options.DefaultPolicy 设置正常。有什么想法吗?
  • 这是一个非常有帮助的答案,并将我在各地看到的很多东西拼凑在一起。
  • @TylerOhlsen 这不正确;虽然它将在您描述的情况下使用,但它不是唯一的。如果您未在端点级别指定授权要求,但使用空的 [Authorize] 属性装饰 MVC 控制器和/或操作,也将使用它。
【解决方案2】:

这是 Mickaël Derriey 答案的延伸。

我们的应用有一个自定义授权要求,我们从内部来源解决。我们使用的是 Auth0,但正在使用 OpenID 切换到 Microsoft 帐户身份验证。这是我们的 ASP.Net Core 2.1 Startup 中经过稍微编辑的代码。对于未来的读者,这在撰写本文时适用于指定的版本。调用者在传入请求中使用来自 OpenID 的 id_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 检查 cfg 中的颁发者是否与您的令牌中的颁发者相同。

    博客文章建议为每个方案使用单独的处理程序,这似乎不需要,覆盖 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://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
                    //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://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication
                     //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;
        }
    }
    

    }

    【讨论】:

      猜你喜欢
      • 2021-11-24
      • 2019-05-22
      • 2018-11-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-18
      • 2016-06-13
      相关资源
      最近更新 更多