validationGoogle OpenID Connect JWT ID令牌

我正在尝试升级我的MVC网站以使用新的OpenID Connect标准。 OWIN中间件看起来非常强大,但不幸的是只支持“form_post”响应类型。 这意味着Google不兼容,因为它会在“#”后面的URL中返回所有令牌,因此它们永远不会到达服务器并且永远不会触发中间件。

我试图在中间件中自己触发响应处理程序,但这似乎根本不起作用,所以我有一个简单的javascript文件解析返回的声明并将它们发送到控制器动作进行处理。

问题是,即使我在服务器端获取它们,我也无法正确解析它们。 我得到的错误看起来像这样:

IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: 'SecurityKeyIdentifier ( IsReadOnly = False, Count = 1, Clause[0] = System.IdentityModel.Tokens.NamedKeySecurityKeyIdentifierClause ), token: '{ "alg":"RS256", "kid":"073a3204ec09d050f5fd26460d7ddaf4b4ec7561" }. { "iss":"accounts.google.com", "sub":"100330116539301590598", "azp":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com", "nonce":"7c8c3656118e4273a397c7d58e108eb1", "email_verified":true, "aud":"1061880999501-b47blhmmeprkvhcsnqmhfc7t20gvlgfl.apps.googleusercontent.com", "iat":1429556543,"exp\":1429560143 }'." } 

我的令牌validation码遵循开发IdentityServer的优秀人员概述的示例

  private async Task<IEnumerable> ValidateIdentityTokenAsync(string idToken, string state) { // New Stuff var token = new JwtSecurityToken(idToken); var jwtHandler = new JwtSecurityTokenHandler(); byte[][] certBytes = getGoogleCertBytes(); for (int i = 0; i < certBytes.Length; i++) { var certificate = new X509Certificate2(certBytes[i]); var certToken = new X509SecurityToken(certificate); // Set up token validation var tokenValidationParameters = new TokenValidationParameters(); tokenValidationParameters.ValidAudience = googleClientId; tokenValidationParameters.IssuerSigningToken = certToken; tokenValidationParameters.ValidIssuer = "accounts.google.com"; try { // Validate SecurityToken jwt; var claimsPrincipal = jwtHandler.ValidateToken(idToken, tokenValidationParameters, out jwt); if (claimsPrincipal != null) { // Valid idTokenStatus = "Valid"; } } catch (Exception e) { if (idTokenStatus != "Valid") { // Invalid? } } } return token.Claims; } private byte[][] getGoogleCertBytes() { // The request will be made to the authentication server. WebRequest request = WebRequest.Create( "https://www.googleapis.com/oauth2/v1/certs" ); StreamReader reader = new StreamReader(request.GetResponse().GetResponseStream()); string responseFromServer = reader.ReadToEnd(); String[] split = responseFromServer.Split(':'); // There are two certificates returned from Google byte[][] certBytes = new byte[2][]; int index = 0; UTF8Encoding utf8 = new UTF8Encoding(); for (int i = 0; i  0) { int startSub = split[i].IndexOf(beginCert); int endSub = split[i].IndexOf(endCert) + endCert.Length; certBytes[index] = utf8.GetBytes(split[i].Substring(startSub, endSub).Replace("\\n", "\n")); index++; } } return certBytes; } 

我知道签名validation对于JWT来说并不是完全必要的,但我对如何关闭它没有任何想法。 有任何想法吗?

问题是JWT中的kid ,其价值是用于签署JWT的密钥的关键标识符。 由于您从JWKs URI手动构造证书数组,因此会丢失密钥标识符信息。 然而,validation程序需要它。

您需要将tokenValidationParameters.IssuerSigningKeyResolver设置为一个函数,该函数将返回您在tokenValidationParameters.IssuerSigningToken设置的相同键。 此委托的目的是指示运行时忽略任何“匹配”语义并尝试键。

有关更多信息,请参阅此文章: JwtSecurityTokenHandler 4.0.0重大更改?

编辑:代码:

 tokenValidationParameters.IssuerSigningKeyResolver = (arbitrarily, declaring, these, parameters) => { return new X509SecurityKey(certificate); }; 

我想我会发布稍微改进的版本,它使用JSON.Net解析Googles的X509证书,并根据“kid”(key-id)匹配要使用的密钥。 这比尝试每个证书更有效,因为非对称加密通常非常昂贵。

还删除了过时的WebClient和手动字符串解析代码:

  static Lazy> Certificates = new Lazy>( FetchGoogleCertificates ); static Dictionary FetchGoogleCertificates() { using (var http = new HttpClient()) { var json = http.GetStringAsync( "https://www.googleapis.com/oauth2/v1/certs" ).Result; var dictionary = JsonConvert.DeserializeObject>( json ); return dictionary.ToDictionary( x => x.Key, x => new X509Certificate2( Encoding.UTF8.GetBytes( x.Value ) ) ); } } JwtSecurityToken ValidateIdentityToken( string idToken ) { var token = new JwtSecurityToken( idToken ); var jwtHandler = new JwtSecurityTokenHandler(); var certificates = Certificates.Value; try { // Set up token validation var tokenValidationParameters = new TokenValidationParameters(); tokenValidationParameters.ValidAudience = _clientId; tokenValidationParameters.ValidIssuer = "accounts.google.com"; tokenValidationParameters.IssuerSigningTokens = certificates.Values.Select( x => new X509SecurityToken( x ) ); tokenValidationParameters.IssuerSigningKeys = certificates.Values.Select( x => new X509SecurityKey( x ) ); tokenValidationParameters.IssuerSigningKeyResolver = ( s, securityToken, identifier, parameters ) => { return identifier.Select( x => { if (!certificates.ContainsKey( x.Id )) return null; return new X509SecurityKey( certificates[ x.Id ] ); } ).First( x => x != null ); }; SecurityToken jwt; var claimsPrincipal = jwtHandler.ValidateToken( idToken, tokenValidationParameters, out jwt ); return (JwtSecurityToken)jwt; } catch (Exception ex) { _trace.Error( typeof( GoogleOAuth2OpenIdHybridClient ).Name, ex ); return null; } } 

Microsoft的人员发布了支持OpenId Connect的Azure V2 B2C Preview端点的代码示例。 看到这里 ,使用帮助类OpenIdConnectionCachingSecurityTokenProvider,代码简化如下:

 app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions { AccessTokenFormat = new JwtFormat(new TokenValidationParameters { ValidAudiences = new[] { googleClientId }, }, new OpenIdConnectCachingSecurityTokenProvider("https://accounts.google.com/.well-known/openid-configuration"))}); 

此类是必需的,因为OAuthBearer中间件不会利用。 默认情况下由STS公开的OpenID Connect元数据端点。

 public class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityTokenProvider { public ConfigurationManager _configManager; private string _issuer; private IEnumerable _tokens; private readonly string _metadataEndpoint; private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim(); public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint) { _metadataEndpoint = metadataEndpoint; _configManager = new ConfigurationManager(metadataEndpoint); RetrieveMetadata(); } ///  /// Gets the issuer the credentials are for. ///  ///  /// The issuer the credentials are for. ///  public string Issuer { get { RetrieveMetadata(); _synclock.EnterReadLock(); try { return _issuer; } finally { _synclock.ExitReadLock(); } } } ///  /// Gets all known security tokens. ///  ///  /// All known security tokens. ///  public IEnumerable SecurityTokens { get { RetrieveMetadata(); _synclock.EnterReadLock(); try { return _tokens; } finally { _synclock.ExitReadLock(); } } } private void RetrieveMetadata() { _synclock.EnterWriteLock(); try { OpenIdConnectConfiguration config = _configManager.GetConfigurationAsync().Result; _issuer = config.Issuer; _tokens = config.SigningTokens; } finally { _synclock.ExitWriteLock(); } } } 

根据Johannes Rudolph的回答,我发布了我的解决方案。 IssuerSigningKeyResolver委托中存在编译器错误,我必须解决。

这是我现在的工作代码:

 using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net.Http; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; namespace QuapiNet.Service { public class JwtTokenValidation { public async Task> FetchGoogleCertificates() { using (var http = new HttpClient()) { var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs"); var dictionary = await response.Content.ReadAsAsync>(); return dictionary.ToDictionary(x => x.Key, x => new X509Certificate2(Encoding.UTF8.GetBytes(x.Value))); } } private string CLIENT_ID = "xxxxx.apps.googleusercontent.com"; public async Task ValidateToken(string idToken) { var certificates = await this.FetchGoogleCertificates(); TokenValidationParameters tvp = new TokenValidationParameters() { ValidateActor = false, // check the profile ID ValidateAudience = true, // check the client ID ValidAudience = CLIENT_ID, ValidateIssuer = true, // check token came from Google ValidIssuers = new List { "accounts.google.com", "https://accounts.google.com" }, ValidateIssuerSigningKey = true, RequireSignedTokens = true, IssuerSigningKeys = certificates.Values.Select(x => new X509SecurityKey(x)), IssuerSigningKeyResolver = (token, securityToken, kid, validationParameters) => { return certificates .Where(x => x.Key.ToUpper() == kid.ToUpper()) .Select(x => new X509SecurityKey(x.Value)); }, ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = TimeSpan.FromHours(13) }; JwtSecurityTokenHandler jsth = new JwtSecurityTokenHandler(); SecurityToken validatedToken; ClaimsPrincipal cp = jsth.ValidateToken(idToken, tvp, out validatedToken); return cp; } } }