动态追加 OWIN JWT 资源服务器 应用程序客户端(受众)

Posted

技术标签:

【中文标题】动态追加 OWIN JWT 资源服务器 应用程序客户端(受众)【英文标题】:Dynamically append OWIN JWT resource server Application clients (audiences) 【发布时间】:2015-01-30 00:02:27 【问题描述】:

我有一个使用 OWIN JWT 进行身份验证的C# API。

我的startup.cs(我的资源服务器)通过代码配置 OAuth:

public void ConfigureOAuth(IAppBuilder app)

    var issuer = "<the_same_issuer_as_AuthenticationServer.Api>";

    // Api controllers with an [Authorize] attribute will be validated with JWT
    var audiences = DatabaseAccessLayer.GetAllowedAudiences(); // Gets a list of audience Ids, secrets, and names (although names are unused)

    // List the 
    List<string> audienceId = new List<string>();
    List<IIssuerSecurityTokenProvider> providers = new List<IIssuerSecurityTokenProvider>();
    foreach (var aud in audiences) 
        audienceId.Add(aud.ClientId);
        providers.Add(new SymmetricKeyIssuerSecurityTokenProvider(issuer, TextEncodings.Base64Url.Decode(aud.ClientSecret)));
    

    app.UseJwtBearerAuthentication(
        new JwtBearerAuthenticationOptions
        
            AuthenticationMode = AuthenticationMode.Active,
            AllowedAudiences = audienceId.ToArray(),
            IssuerSecurityTokenProviders = providers.ToArray(),
            Provider = new OAuthBearerAuthenticationProvider
            
                OnValidateIdentity = context =>
                
                    context.Ticket.Identity.AddClaim(new System.Security.Claims.Claim("newCustomClaim", "newValue"));
                    return Task.FromResult<object>(null);
                
            
        );

它允许对多个 ClientID 再次检查经过身份验证的承载令牌。 这很好用。 但是,我的 Web 应用程序允许用户创建新的应用程序受众(ie、新的 ClientIDClientSecretClientName 组合),但是发生这种情况后,我不知道如何让资源服务器的JwtBearerAuthenticationOptions 识别新创建的受众。

我可以在新观众之后重新启动服务器,以便 ConfigureOAuth() 在之后重新运行,但从长远来看这不是一个好方法。

有人知道如何添加受众(ie、新的 **ClientIDClientSecretClientName 组合)到 OWIN 应用程序 JwtBearerAuthenticationOptions 之外的 startup.cs 和 ConfigureOAuth()?**

我一直在寻找:https://docs.auth0.com/aspnetwebapi-owin-tutorial 和 http://bitoftech.net/2014/10/27/json-web-token-asp-net-web-api-2-jwt-owin-authorization-server/ 寻求帮助,但两个代码示例都显示了上述相同的问题。

【问题讨论】:

您找到解决方案了吗? 是的,我也需要同样的东西,有消息吗? 【参考方案1】:

当使用 X509CertificateSecurityTokenProvider 时,以下内容有效。已修改为使用 SymmetricKeyIssuerSecurityTokenProvider 但尚未经过测试。

public void ConfigureOAuth(IAppBuilder app)

    var issuer = "<the_same_issuer_as_AuthenticationServer.Api>";

    // Api controllers with an [Authorize] attribute will be validated with JWT
    Func<IEnumerable<Audience>> allowedAudiences = () => DatabaseAccessLayer.GetAllowedAudiences();

    var bearerOptions = new OAuthBearerAuthenticationOptions
    
        AccessTokenFormat = new JwtFormat(new TokenValidationParameters
        
            AudienceValidator = (audiences, securityToken, validationParameters) =>
            
                return allowedAudiences().Select(x => x.ClientId).Intersect(audiences).Count() > 0;
            ,
            ValidIssuers = new ValidIssuers  Audiences = allowedAudiences ,
            IssuerSigningTokens = new SecurityTokensTokens(issuer)  Audiences = allowedAudiences 
        )
    ;
    app.UseOAuthBearerAuthentication(bearerOptions);


public abstract class AbstractAudiences<T> : IEnumerable<T>

    public Func<IEnumerable<Audience>> Audiences  get; set; 

    public abstract IEnumerator<T> GetEnumerator();

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    
        throw new NotImplementedException();
    


public class SecurityTokensTokens : AbstractAudiences<SecurityToken>

    private string issuer;

    public SecurityTokensTokens(string issuer)
    
        this.issuer = issuer;
    

    public override IEnumerator<SecurityToken> GetEnumerator()
    
        foreach (var aud in Audiences())
        
            foreach (var securityToken in new SymmetricKeyIssuerSecurityTokenProvider(issuer, TextEncodings.Base64Url.Decode(aud.ClientSecret)).SecurityTokens)
            
                yield return securityToken;
            ;
        
    


public class ValidIssuers : AbstractAudiences<string>

    public override IEnumerator<string> GetEnumerator()
    
        foreach (var aud in Audiences())
        
            yield return aud.ClientSecret;
        
    

【讨论】:

嗨,丹尼。关于 WebAPI 上的 JWT 授权的总菜鸟。我遵循了与 Brett 相同的教程并尝试了您的解决方案,但是使用您的代码我似乎无法进入任何用 [Authorize] 装饰的端点。我仍在使用bitoftech.net/2014/10/27/… 中的代码来生成我的 JWT,并且在使用该示例时它工作正常。显然,尽管我不能只拥有一个允许的观众,并且希望获得一个动态的解决方案来工作。您能否分享更多您的代码? @MrThursday 如果您使用单个发行者签署令牌,如果您将 ValidIssuers 更改为 ValidIssuer 并分配给您的单个发行者,这将起作用。 Func 是这里的魔法。我也是从 bitoftech 文章开始的。 @technicallyjosh 您是否有机会通过将 ValidIssuers 更改为 ValidIssuer 来详细说明您的意思?我的令牌来自一个发行者,但我有多个客户/客户机密。如果你能分享一些代码,那将是巨大的。 据我所知,虽然这个解决方案在技术上可行,但它需要为每个请求往返数据库。如果有一种方法可以在每次新客户端注册时更新 AllowedAudiences 和 IssueSecurityTokenProviders,这样数据库就不需要被每个传入的请求击中。【参考方案2】:

我会尽力提供帮助:D 请记住,我是初学者,所以它可能不是最好的:D

我还希望在不重新启动我的服务的情况下拥有动态受众,因为这最终涉及灵活性和易用性。

因此我的验证如下:

        var bearerOptions = new OAuthBearerAuthenticationOptions
        
            AccessTokenFormat = new JwtFormat(new TokenValidationParameters
            
                AudienceValidator = AudienceValidator,
                IssuerSigningToken = x509SecToken,
                ValidIssuer = issuer,
                RequireExpirationTime = true,
                ValidateLifetime = true,                    
            )
        ;
        app.UseOAuthBearerAuthentication(bearerOptions);

正如您在上面看到的,我确实有一个代表正在验证我的听众。这基本上意味着 - 每次您向服务器发出请求时,都会调用此方法来验证受众。

目前我只有一些小的调试方法,并且我正在验证 任何 观众进来:

    private bool AudienceValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
    
        Trace.Write("would be validating audience now");
        return true;
    

现在下一步是在这里做什么?可以肯定的是,您不想在每次验证观众时都查询数据库,因为那样会忽略使用这些令牌的目的:D 您可能会想出一些好主意 - 请分享!

第一种方法:

所以我所做的是使用https://github.com/jgeurts/FluentScheduler,并且我已安排每 1 小时从 DB 更新 AllowedAudiences。这很好用。我正在注册具有一系列权利的新观众,在最好的情况下,他们已经准备好起飞,或者我必须等待大约 59 分钟 :)

希望这会有所帮助!

第二种方法:

现在,我已经向定义授权资源的 JWT 令牌添加了声明。然后我正在检查安全令牌中是否有与我的资源服务器匹配的资源。如果是这样,我们认为观众是经过验证的:D

【讨论】:

【参考方案3】:

我们遇到了同样的问题,走的是同样的路。此外,我还尝试制作自定义 OAuthBearerAuthenticationProvider() 传递 IAppBuilder 和 JwtBearerAuthenticationOptions 对象,覆盖 OnValidateIdentity() 并在那里重新加载 JwtBearerAuthenticationOptions 但新受众仍未验证。

我想,我现在会坚持应用程序重启来解决这个问题。

希望这将为其他人指明正确的道路。

【讨论】:

【参考方案4】:

我们还需要动态 JWT 受众处理程序,专门针对 Azure B2C 租户。租户信息存储在数据库中,该数据库用于为每个租户配置单独的 OAuthBearerAuthenticationProvider() 条目和 B2C 策略(使用 B2C 租户所需的附加参数)。

我们发现,通过在启动后尝试使用 IAppBuilder UseOAuthBearerAuthentication() 添加其他条目根本不起作用 - 未正确管理的提供程序因此未检索到签名令牌,从而导致 HTTP 401 质询。 (我们保留了IAppBuiler 对象,以便以后使用。)

查看验证令牌的JwtFormat.cs 代码提供了有关如何实施解决方案的线索(我们在版本 3.1.0 - YMMV):

https://github.com/aspnet/AspNetKatana/blob/v3.1.0/src/Microsoft.Owin.Security.Jwt/JwtFormat.cs#L193

这是它从提供的OAuthBearerAuthenticationProvider() 中提取颁发者和签名密钥的地方。请注意,这对于我们的目的来说效率有点低 - 它会提取所有颁发者和签名密钥,即使只有一个受众会匹配 Azure B2C 租户颁发的 JWT。

我们所做的是:

    只使用一个UseOAuthBearerAuthentication() 调用而不使用OAuthBearerAuthenticationProvider() - 只需传递 TokenValidationParameters; 使用子类JwtSecurityTokenHandler 并覆盖ValidateToken 以动态管理受众; 创建子类JwtSecurityTokenHandler 的实例并将其插入JwtFormat.TokenHandler

如何管理和开始添加新受众取决于您自己。我们使用数据库和 Redis 来传递 reload 命令。

这里是 Startup.Auth.cs sn-p:

/// <summary>
/// The B2C token handler for handling dynamically loaded B2C tenants.
/// </summary>
protected B2CTokenHandler TokenHandler = new B2CTokenHandler();

/// <summary>
/// Setup the OAuth authentication. We use the database to retrieve the available B2C tenants.
/// </summary>
/// <param name="app">The application builder object</param>
public AuthOAuth2(IAppBuilder app) 
    // get Active Directory endpoint
    AadInstance = ConfigurationManager.AppSettings["b2c:AadInstance"];

    // get the B2C policy list used by API1
    PolicyIdList = ConfigurationManager.AppSettings["b2c:PolicyIdList"].Split(',').Select(p => p.Trim()).ToList();

    TokenValidationParameters tvps = new TokenValidationParameters 
        NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"
    ;

    // create a access token format 
    JwtFormat jwtFormat = new JwtFormat(tvps);

    // add our custom token handler which will provide token validation parameters per tenant
    jwtFormat.TokenHandler = TokenHandler;

    // wire OAuth authentication for tenants
    app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions 
        // the security token provider handles Azure AD B2C metadata & signing keys from the OpenIDConnect metadata endpoint
        AccessTokenFormat = jwtFormat,
        Provider = new OAuthBearerAuthenticationProvider() 
            OnValidateIdentity = async (context) => await OAuthValidateIdentity(context)
        
    );

    // load initial OAuth authentication tenants
    LoadAuthentication();


/// <summary>
/// Load the OAuth authentication tenants. We maintain a local hash map of those tenants during
/// processing so we can track those tenants no longer in use.
/// </summary>
protected override void LoadAuthentication() 
    AuthProcessing authProcessing = new AuthProcessing();

    List<B2CAuthTenant> authTenantList = new List<B2CAuthTenant>();

    // add all tenants for authentication
    foreach (AuthTenantApp authTenantApp in authProcessing.GetAuthTenantsByAppId("API1")) 
        // create a B2C authentication tenant per policy. Note that the policy may not exist, and
        // this will be handled by the B2C token handler at configuration load time below
        foreach (string policyId in PolicyIdList) 
            authTenantList.Add(new B2CAuthTenant 
                Audience = authTenantApp.ClientId,
                PolicyId = policyId,
                TenantName = authTenantApp.Tenant
            );
        
    

    // and load the token handler with the B2C authentication tenants
    TokenHandler.LoadConfiguration(AadInstance, authTenantList);

    // we must update the CORS origins
    string origins = string.Join(",", authProcessing.GetAuthTenantAuthoritiesByAppId("API1").Select(a => a.AuthorityUri));

    // note some browsers do not support wildcard for exposed headers - there specific needed. See
    //
    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers#Browser_compatibility
    EnableCorsAttribute enableCors = new EnableCorsAttribute(origins, "*", "*", "Content-Disposition");
    enableCors.SupportsCredentials = true;
    enableCors.PreflightMaxAge = 30 * 60;

    GlobalConfiguration.Configuration.EnableCors(enableCors);

这是被覆盖的 JwtSecurityTokenHandler 类的 sn-p:

/// <summary>
/// Dictionary of currently configured OAuth audience+policy to the B2C endpoint signing key cache.
/// </summary>
protected ConcurrentDictionary<string, OpenIdConnectCachingSecurityTokenProvider> AudiencePolicyMap = new ConcurrentDictionary<string, OpenIdConnectCachingSecurityTokenProvider>();

/// <summary>
/// Load the B2C authentication tenant list, creating a B2C endpoint security token provider
/// which will bethe source of the token signing keys.
/// </summary>
/// <param name="aadInstance">The Active Directory instance endpoint URI</param>
/// <param name="b2cAuthTenantList">The B2C authentication tenant list</param>
public void LoadConfiguration(string aadInstance, List<B2CAuthTenant> b2cAuthTenantList) 
    // maintain a list of keys that are loaded
    HashSet<string> b2cAuthTenantSet = new HashSet<string>();

    // attempt to create a security token provider for each authentication tenant
    foreach(B2CAuthTenant b2cAuthTenant in b2cAuthTenantList) 
        // form the dictionary key
        string tenantKey = $"b2cAuthTenant.Audience:b2cAuthTenant.PolicyId";

        if (!AudiencePolicyMap.ContainsKey(tenantKey)) 
            try 
                // attempt to create a B2C endpoint security token provider. We may fail if there is no policy 
                // defined for that tenant
                OpenIdConnectCachingSecurityTokenProvider tokenProvider = new OpenIdConnectCachingSecurityTokenProvider(String.Format(aadInstance, b2cAuthTenant.TenantName, b2cAuthTenant.PolicyId));

                // add to audience:policy map
                AudiencePolicyMap[tenantKey] = tokenProvider;

                // this guy is new
                b2cAuthTenantSet.Add(tenantKey);
             catch (Exception ex) 
                // exception has already been reported appropriately
            
         else 
            // this guys is already present
            b2cAuthTenantSet.Add(tenantKey);
        
    

    // at this point we have a set of B2C authentication tenants that still exist. Remove any that are not
    foreach (KeyValuePair<string, OpenIdConnectCachingSecurityTokenProvider> kvpAudiencePolicy in AudiencePolicyMap.Where(t => !b2cAuthTenantSet.Contains(t.Key))) 
        AudiencePolicyMap.TryRemove(kvpAudiencePolicy.Key, out _);
    


/// <summary>
/// Validate a security token. We are responsible for priming the token validation parameters
/// with the specific parameters for the audience:policy, if found.
/// </summary>
/// <param name="securityToken">A 'JSON Web Token' (JWT) that has been encoded as a JSON object. May be signed using 'JSON Web Signature' (JWS)</param>
/// <param name="tvps">Contains validation parameters for the security token</param>
/// <param name="validatedToken">The security token that was validated</param>
/// <returns>A claims principal from the jwt. Does not include the header claims</returns>
public override ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters tvps, out SecurityToken validatedToken) 
    if (string.IsNullOrWhiteSpace(securityToken)) 
        throw new ArgumentNullException("Security token is null");
    

    // decode the token as we need the 'aud' and 'tfp' claims
    JwtSecurityToken token = ReadToken(securityToken) as JwtSecurityToken;

    if (token == null) 
        throw new ArgumentOutOfRangeException("Security token is invalid");
    

    // get the audience and policy
    Claim audience = token.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.Aud);
    Claim policy = token.Claims.FirstOrDefault(c => c.Type == ClaimTypesB2C.Tfp);

    if ((audience == null) || (policy == null)) 
        throw new SecurityTokenInvalidAudienceException("Security token has no audience/policy id");
    

    // generate the key
    string tenantKey = $"audience.Value:policy.Value";

    // check if this audience:policy is known
    if (!AudiencePolicyMap.ContainsKey(tenantKey)) 
        throw new SecurityTokenInvalidAudienceException("Security token has unknown audience/policy id");
    

    // get the security token provider
    OpenIdConnectCachingSecurityTokenProvider tokenProvider = AudiencePolicyMap[tenantKey];

    // clone the token validation parameters so we can update
    tvps = tvps.Clone();

    // we now need to prime the validation parameters for this audience
    tvps.ValidIssuer = tokenProvider.Issuer;
    tvps.ValidAudience = audience.Value;
    tvps.AuthenticationType = policy.Value;
    tvps.IssuerSigningTokens = tokenProvider.SecurityTokens;

    // and call real validator with updated parameters
    return base.ValidateToken(securityToken, tvps, out validatedToken);

对于我们的 B2C 租户,并非所有可用策略都为租户定义。我们需要在OpenIdConnectCachingSecurityTokenProvider 处理这个问题:

/// <summary>
/// Retrieve the metadata from the endpoint.
/// </summary>
private void RetrieveMetadata() 
    metadataLock.EnterWriteLock();

    try 
        // retrieve the metadata
        OpenIdConnectConfiguration config = Task.Run(configManager.GetConfigurationAsync).Result;

        // and update
        issuer = config.Issuer;
        securityTokens = config.SigningTokens;
     catch (Exception ex) when (CheckHttp404(ex)) 
        // ignore 404 errors as they indicate that the policy does not exist for a tenant
        logger.Warn($"Policy endpoint not found for metadataEndpoint - ignored");
        throw ex;
     catch (Exception ex) 
        logger.Fatal(ex, $"System error in retrieving token metadatafor metadataEndpoint");
        throw ex;
     finally 
        metadataLock.ExitWriteLock();
    


/// <summary>
/// Check if the inner most exception is a HTTP response with status code of Not Found.
/// </summary>
/// <param name="ex">The exception being examined for a 404 status code</param>
/// <returns></returns>
private bool CheckHttp404(Exception ex) 
    // get the inner most exception
    while(ex.InnerException != null) 
        ex = ex.InnerException;
    

    // check if a HttpWebResponse with a 404
    return (ex is WebException webex) && (webex.Response is HttpWebResponse response) && (response.StatusCode == HttpStatusCode.NotFound);

【讨论】:

以上是关于动态追加 OWIN JWT 资源服务器 应用程序客户端(受众)的主要内容,如果未能解决你的问题,请参考以下文章

具有混合身份验证 JWT 和 SAML 的 ASP.NET Web API 2.2 OWIN

openid connect owin 如何验证来自 Azure AD 的令牌?

带有 Owin JWT 身份的 MVC

修改 OWIN OAuth 中间件以使用 JWT 不记名令牌

如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?

支持 JWT 密钥轮换的承载令牌认证的 Owin 中间件