【问题标题】:Dynamically append OWIN JWT resource server Application clients (audiences)动态追加 OWIN JWT 资源服务器 应用程序客户端(受众)
【发布时间】: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-tutorialhttp://bitoftech.net/2014/10/27/json-web-token-asp-net-web-api-2-jwt-owin-authorization-server/ 寻求帮助,但两个代码示例都显示了上述相同的问题。

【问题讨论】:

  • 您找到解决方案了吗?
  • 是的,我也需要同样的东西,有消息吗?

标签: c# oauth owin jwt


【解决方案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。

      我们所做的是:

      1. 只使用一个UseOAuthBearerAuthentication() 调用而不使用OAuthBearerAuthenticationProvider() - 只需传递 TokenValidationParameters;
      2. 使用子类JwtSecurityTokenHandler 并覆盖ValidateToken 以动态管理受众;
      3. 创建子类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);
      }
      

      【讨论】:

        猜你喜欢
        • 2016-06-23
        • 1970-01-01
        • 2017-01-30
        • 1970-01-01
        • 2019-01-18
        • 2019-05-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多