2015-04-20 13 views
11

Próbuję uaktualnić moją witrynę MVC, aby korzystać z nowego standardu OpenID Connect. Warstwa pośrednia OWIN wydaje się być dość solidna, ale niestety obsługuje tylko typ odpowiedzi "form_post" . Oznacza to, że Google nie jest kompatybilny, ponieważ zwraca wszystkie tokeny w adresie URL po "#", więc nigdy nie docierają do serwera i nigdy nie uruchamiają oprogramowania pośredniego.Sprawdzanie poprawności identyfikatora Google OpenID Connect Identyfikator JWT

Próbowałem wywołać procedury obsługi odpowiedzi w oprogramowaniu pośredniczącym, ale to w ogóle nie działa, więc mam prosty plik javascript, który analizuje zwrócone żądania i POST je do kontrolera akcja do przetworzenia.

Problem polega na tym, że nawet jeśli dostaję je po stronie serwera, nie mogę ich poprawnie przeanalizować. Błąd pojawia wygląda następująco:

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

Moja tokena kod weryfikacyjny następujący przykład określone przez dobrych ludzi rozwijających 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; 
    } 

wiem, że walidacja Podpis nie jest całkowicie niezbędne do JWTs ale nie mam najmniejszego pojęcia, jak to wyłączyć. Jakieś pomysły?

Odpowiedz

6

Problem polega na tym, że kid w JWT, którego wartość jest kluczowym identyfikatorem klucza, użyto do podpisania JWT. Ponieważ ręcznie tworzysz tablicę certyfikatów z identyfikatora URI JWKs, tracisz informacje o identyfikatorze klucza. Jednak procedura zatwierdzania tego wymaga.

Musisz ustawić tokenValidationParameters.IssuerSigningKeyResolver dla funkcji, która zwróci ten sam klucz, który ustawiłeś powyżej w tokenValidationParameters.IssuerSigningToken. Celem tego delegata jest poinstruowanie środowiska wykonawczego, aby zignorował każdą "pasującą" semantykę i po prostu spróbuj tego klucza.

Zobacz ten artykuł, aby uzyskać więcej informacji: JwtSecurityTokenHandler 4.0.0 Breaking Changes?

EDIT: kod:

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

Gdy zorientowali się, jak to zrobić, to działało idealnie. Dzięki za pomoc. Kod wygląda tak: 'tokenValidationParameters.IssuerSigningKeyResolver = (arbitralnie, deklarowanie, te, parametry) => { zwraca nowy X509SecurityKey (certyfikat); }; ' – ReimTime

+0

thx, dodano do odpowiedzi na kompletność –

6

Myślałam, że po mojej lekko ulepszoną wersję, która używa Json.NET do analizowania gogli X509 i Certyfikaty dopasowuje klucz do użycia na podstawie "kid" (key-id). Jest to nieco bardziej skuteczne niż testowanie każdego certyfikatu, ponieważ asymetryczny krypto jest zwykle dość drogi.

usunięto także przestarzałych WebClient i ręczne kod ciąg parsowania:

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

Dziękuję bardzo za fragment kodu! Nadal zastanawiam się, czy istnieje sposób na wygenerowanie tych publicznych kluczy/certyfikatów z odpowiedzi https://www.googleapis.com/oauth2/v3/certs (wypróbowałem to za pomocą RSACryptoServiceProvider, ale niestety nie udało się.) – Robar

+1

@Robar : czy punkt końcowy v1 zniknie w najbliższym czasie? Inną rzeczą, którą zauważyłem jest to, że google obraca certyfikaty o codziennie, więc musisz radzić sobie z chybieniami w pamięci podręcznej, a następnie odzyskać certyfikaty. –

+0

Mam nadzieję, że nie, ale obecny "jwks_uri" dokumentu odkrywczego to punkt końcowy v3 (zobacz https://accounts.google.com/ .well-known/openid-configuration). Rozwiązałem już problem z obracającymi się certyfikatami, umieszczając certyfikaty w pamięci podręcznej z czasem wygaśnięcia. Pobieram czas wygaśnięcia z żądania HTTP, które pobiera certyfikaty, odpowiedź HTTP ma ustawiony zestaw "max-age". Dodatkowo wykonuję jedno ponowne pobranie certyfikatów, jeśli sprawdzanie poprawności nie powiedzie się za pierwszym razem. – Robar

1

Ludzie na Microsoft zamieszczonych przykładowy kod dla Azure V2 B2C podglądu końcowego, które obsługują OpenID Connect. Zobacz here z klasy pomocnika OpenIdConnectionCachingSecurityTokenProvider kod jest uproszczone następująco:

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

Ta klasa jest konieczne, ponieważ OAuthBearer Middleware nie wykorzystać. Punkt końcowy metadanych OpenID jest domyślnie odsłonięty przez STS.

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

Na podstawie odpowiedzi Johannesa Rudolpha zamieszczam moje rozwiązanie. Wystąpił błąd kompilatora w Delegaturze IssuerSigningKeyResolver, który musiałem rozwiązać.

To jest mój kod roboczych teraz:

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; 
     } 
    } 
} 
Powiązane problemy