使用多个 JWT 承载者身份验证

有没有可能在 ASP.NET Core 2中支持多个 JWT 令牌发行者? 我想为外部服务提供一个 API,并且需要使用两个 JWT 令牌源—— Firebase 和自定义 JWT 令牌发行者。在 ASP.NET 核心中,我可以为 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
};
}

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

61583 次浏览

你完全可以得到你想要的:

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直到它得到匹配引起的。通常可以忽略该错误。

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

我们的应用程序有一个自定义的授权要求,我们从一个内部来源解决。我们使用的是 Auth0,但是正在切换到使用 OpenID 的 Microsoft 帐户身份验证。下面是我们的 ASP.Net Core 2.1启动程序稍作编辑的代码。对于未来的读者,在撰写本文时,这对指定的版本是有效的。调用方对作为 Bearer 令牌传递的传入请求使用来自 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());
});

你的问题的解决方案,可以在下面的博客文章 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;
}
}

}

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

[ Authorize (AuthenticationScheme = “ Firebase,Custom”,Policy = “ FirebaseAdministrator”)]

如果没有提供身份验证方案,并且 AddAuthentication ()没有参数,NetCore 将无法进行身份验证和请求。HttpContext.用户。IsAuthenticated 设置为 false