2015-04-20 17 views
11

Sto tentando di aggiornare il sito Web MVC per utilizzare il nuovo standard OpenID Connect. Il middleware OWIN sembra essere piuttosto robusto, ma sfortunatamente supporta solo il tipo di risposta "form_post" . Ciò significa che Google non è compatibile, poiché restituisce tutti i token nell'URL dopo un "#", in modo che non raggiungano mai il server e non attivino mai il middleware.Convalida il token ID JWT di OpenID Connect di Google

Ho provato ad attivare i gestori di risposta nel middleware, ma non sembra funzionare affatto, quindi ho un semplice file javascript che analizza le attestazioni restituite e le invia POST a un controller azione per l'elaborazione.

Il problema è che, anche quando li ottengo sul lato server, non posso analizzarli correttamente. L'errore che ottengo appare così:

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 
    }'." 
} 

mio codice di verifica di token segue l'esempio tracciato dalle buone persone in via di sviluppo 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; 
    } 

so che la convalida della firma non è del tutto necessaria per JWTs ma io non ho la minima idea di come spegnerlo. Qualche idea?

risposta

6

Il problema è il kid nel JWT il cui valore è l'identificatore chiave della chiave utilizzato per firmare il JWT. Poiché si costruisce manualmente una serie di certificati dall'URI JWK, si perdono le informazioni sull'identificativo della chiave. La procedura di convalida tuttavia lo richiede.

È necessario impostare tokenValidationParameters.IssuerSigningKeyResolver su una funzione che restituirà la stessa chiave impostata in precedenza in tokenValidationParameters.IssuerSigningToken. Lo scopo di questo delegato è di insegnare al runtime di ignorare qualsiasi semantica 'corrispondente' e provare semplicemente la chiave.

veda questo articolo per maggiori informazioni: JwtSecurityTokenHandler 4.0.0 Breaking Changes?

Edit: il codice:

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

Una volta ho capito come si fa, questo ha funzionato perfettamente. Grazie per l'aiuto. Il codice si presenta così: 'tokenValidationParameters.IssuerSigningKeyResolver = (arbitrariamente, dichiarando, questi, parametri) => { return new X509SecurityKey (certificato); }; ' – ReimTime

+0

thx, aggiunto alla risposta per completezza –

6

ho pensato di postare il mio leggermente migliorata versione che utilizza JSON.Net per analizzare Googles' X509 certificati e corrisponde alla chiave da utilizzare in base al "capretto" (id-chiave). Questo è un po 'più efficiente di provare ogni certificato, poiché la crittografia asimmetrica è in genere piuttosto costosa.

rimossi anche WebClient antiquato e manuale Codice stringa di analisi:

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

Grazie mille per il tuo snippet di codice! Mi sto ancora chiedendo se c'è un modo per generare tali chiavi/certificati pubblici dalla risposta di https://www.googleapis.com/oauth2/v3/certs (provato con RSACryptoServiceProvider, ma purtroppo fallito). – Robar

+1

@Robar : l'endpoint v1 andrà via in qualsiasi momento presto? Un'altra cosa che ho notato è che google fa girare i certificati circa ogni giorno, quindi è necessario occuparsi di errori di cache e quindi recuperare i certificati. –

+0

Speriamo di no, ma l'attuale 'jwks_uri' del documento di scoperta è l'endpoint v3 (vedere https://accounts.google.com/.well-known/openid-configuration). Ho già gestito il problema con i certificati di rotazione, inserendo i certificati in una cache con una scadenza. Recupero il tempo di scadenza dalla richiesta HTTP che ottiene i certificati, la risposta HTTP ha un set 'max-age'. Inoltre faccio un re-recupero dei certificati se la validazione fallisce al primo tentativo. – Robar

1

I ragazzi di Microsoft hanno registrato esempio di codice per Azure V2 B2C Anteprima endpoint che supportano OpenID Connect. Vedere here, con la classe helper OpenIdConnectionCachingSecurityTokenProvider il codice è semplificata come segue:

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

Questa classe è necessario perché l'OAuthBearer Middleware non sfruttare. L'endpoint dei metadati OpenID Connect esposto da STS per impostazione predefinita.

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

Sulla base della risposta di Johannes Rudolph, posta la mia soluzione. C'è un errore del compilatore nel delegato IssuerSigningKeyResolver che ho dovuto risolvere.

Questo è il mio codice di lavoro ora:

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; 
     } 
    } 
} 
Problemi correlati