2015-04-20 47 views
11

我正在尝试升级我的MVC网站以使用新的OpenID Connect标准。 OWIN中间件似乎非常强大,但不幸的是只支持 “form_post”响应类型。这意味着Google不兼容,因为它会在“#”后返回url中的所有令牌,所以它们永远不会到达服务器,也不会触发中间件。验证Google OpenID Connect智威汤逊ID令牌

我试图在中间件中自己触发响应处理程序,但这似乎并没有工作,所以我有一个简单的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 
    }'." 
} 

我的令牌验证码遵循良好的人开发IdentityServer

private async Task<IEnumerable<Claim>> 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 < split.Length; i++) 
     { 
      if (split[i].IndexOf(beginCert) > 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; 
    } 

我知道,签名验证是完全没有必要的JWTs但我列出的例子没有丝毫的想法如何关闭它。有任何想法吗?

回答

6

问题是JWT中的kid,其值是该密钥的密钥标识符用于签署JWT。由于您从JWKs URI手动构建了一个证书数组,因此您将失去密钥标识符信息。然而验证程序需要它。

您需要将tokenValidationParameters.IssuerSigningKeyResolver设置为一个函数,该函数将返回与上面在tokenValidationParameters.IssuerSigningToken中设置的相同的密钥。此代理的目的是指示运行时忽略任何“匹配”语义,只需尝试一下密钥。

请参阅本文的详细信息:JwtSecurityTokenHandler 4.0.0 Breaking Changes?

编辑:代码:

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

一旦我想通了,如何做到这一点,这完美地工作。谢谢您的帮助。代码如下所示: 'tokenValidationParameters.IssuerSigningKeyResolver =(任意,声明,这些参数)=> { }返回新的X509SecurityKey(证书); };' – ReimTime

+0

thx,添加到答案完整性 –

6

我想我会后我的,它​​使用JSON.Net解析谷歌X509证书略有改善版本匹配基于“kid”(key-id)使用的密钥。这比尝试每个证书更有效率,因为非对称加密通常非常昂贵。

还删除了过时的Web客户端和手动字符串解析代码:

static Lazy<Dictionary<string, X509Certificate2>> Certificates = new Lazy<Dictionary<string, X509Certificate2>>(FetchGoogleCertificates); 
    static Dictionary<string, X509Certificate2> FetchGoogleCertificates() 
    { 
     using (var http = new HttpClient()) 
     { 
      var json = http.GetStringAsync("https://www.googleapis.com/oauth2/v1/certs").Result; 

      var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(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; 
     } 
    } 
+0

非常感谢您的代码片段!我仍然想知道是否有方法从https://www.googleapis.com/oauth2/v3/certs的响应中生成这些公钥/证书(试用RSACryptoServiceProvider,但不幸失败) – Robar

+1

@Robar :v1的终点是否会很快消失?我注意到的另一件事是,谷歌每天轮换证书,所以你需要处理缓存未命中,然后重新检索证书。 –

+0

希望不是,但发现文档的当前'jwks_uri'是v3端点(请参阅https://accounts.google.com/.well-known/openid-configuration)。我已经通过将证书放入具有到期时间的缓存来解决旋转证书的问题。我从获取证书的HTTP请求中检索到期时间,HTTP响应具有“max-age”集。此外,如果验证在第一次尝试失败,我会重新检索证书。 – Robar

1

在微软的乡亲张贴代码示例为支持OpenID连接Azure的V2 B2C预览端点。见here,与助手类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<OpenIdConnectConfiguration> _configManager; 
    private string _issuer; 
    private IEnumerable<SecurityToken> _tokens; 
    private readonly string _metadataEndpoint; 

    private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim(); 

    public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint) 
    { 
     _metadataEndpoint = metadataEndpoint; 
     _configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint); 

     RetrieveMetadata(); 
    } 

    /// <summary> 
    /// Gets the issuer the credentials are for. 
    /// </summary> 
    /// <value> 
    /// The issuer the credentials are for. 
    /// </value> 
    public string Issuer 
    { 
     get 
     { 
      RetrieveMetadata(); 
      _synclock.EnterReadLock(); 
      try 
      { 
       return _issuer; 
      } 
      finally 
      { 
       _synclock.ExitReadLock(); 
      } 
     } 
    } 

    /// <summary> 
    /// Gets all known security tokens. 
    /// </summary> 
    /// <value> 
    /// All known security tokens. 
    /// </value> 
    public IEnumerable<SecurityToken> 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(); 
     } 
    } 
} 
1

根据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<Dictionary<string, X509Certificate2>> FetchGoogleCertificates() 
     { 
      using (var http = new HttpClient()) 
      { 
       var response = await http.GetAsync("https://www.googleapis.com/oauth2/v1/certs"); 

       var dictionary = await response.Content.ReadAsAsync<Dictionary<string, string>>(); 
       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<ClaimsPrincipal> 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<string> { "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; 
     } 
    } 
}