2014-10-21 7 views
6

Sto tentando di creare una JWT per autorizzare con un account di servizio come descritto in Google documentation utilizzando System.IdentityModel.Tokens.Jwt. Ho il seguente codice:Come produrre JWT con l'algoritmo compatibile con Google OAuth2 RSA SHA-256 utilizzando System.IdentityModel.Tokens.Jwt?

byte[] key = Convert.FromBase64String("..."); 
var certificate = new X509Certificate2(key, "notasecret"); 

DateTime now = DateTime.UtcNow; 
TimeSpan span = now - UnixEpoch; 
Claim[] claims = 
{ 
    new Claim("iss", "[email protected]"), 
    new Claim("scope", "https://www.googleapis.com/auth/plus.me"), 
    new Claim("aud", "https://accounts.google.com/o/oauth2/token"), 
    new Claim("iat", span.TotalSeconds.ToString()), 
    new Claim("exp", span.Add(TimeSpan.FromHours(1)).TotalSeconds.ToString()) 
}; 

JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); 
var descriptor = new SecurityTokenDescriptor 
{ 
    SigningCredentials = new SigningCredentials(
     new InMemorySymmetricSecurityKey(key), 
     "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", 
     "http://www.w3.org/2001/04/xmlenc#sha256"), 
    Subject = new ClaimsIdentity(claims) 
}; 

JwtSecurityToken jwtSecurityToken = (JwtSecurityToken)handler.CreateToken(descriptor); 
string json = handler.WriteToken(jwtSecurityToken); 

quali uscite:

{ "typ" : "JWT" , "alg" : "HS256" } 

Mentre Google afferma esplicitamente che supporta SHA-256:

account di servizio si basano sul RSA SHA-256 Algoritmo e formato token JWT

Secondo wtSecurityTokenHandler.InboundAlgorithmMap:

RS256 => http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 
HS256 => http://www.w3.org/2001/04/xmldsig-more#hmac-sha256 

Così, quando posso cambiare il mio codice:

new SigningCredentials(
    new InMemorySymmetricSecurityKey(key), 
     "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", 
     "http://www.w3.org/2001/04/xmlenc#sha256"); 

sto diventando un'eccezione:

System.InvalidOperationException: IDX10632: SymmetricSecurityKey.GetKeyedHashAlgorithm('http://www.w3.org/2001/04/xmldsig-more#rsa-sha256') threw an exception. 
SymmetricSecurityKey: 'System.IdentityModel.Tokens.InMemorySymmetricSecurityKey' 
SignatureAlgorithm: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', check to make sure the SignatureAlgorithm is supported. 

significa Microsoft non supporta l'algoritmo Google supporta esclusivamente?

+0

Forse provare a utilizzare le costanti integrate per gli algoritmi di firma e digest? (http://msdn.microsoft.com/en-us/library/system.identitymodel.tokens.securityalgorithms%28v=vs.110%29.aspx) –

+0

@Jeff: hey, mi dispiace, ho perso la notifica del tuo commento. Buon punto Ma sfortunatamente non funziona ancora. – abatishchev

risposta

6
private static async Task<string> GetAuthorizationToken(GoogleAuthOptions authOptions) 
{ 
    string jwt = CreateJwt(authOptions); 

    var dic = new Dictionary<string, string> 
    { 
     { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, 
     { "assertion", jwt } 
    }; 
    var content = new FormUrlEncodedContent(dic); 

    var httpClient = new HttpClient { BaseAddress = new Uri("https://accounts.google.com") }; 
    var response = await httpClient.PostAsync("/o/oauth2/token", content); 
    response.EnsureSuccessStatusCode(); 

    dynamic dyn = await response.Content.ReadAsAsync<dynamic>(); 
    return dyn.access_token; 
} 

private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 

private static string CreateJwt(GoogleAuthOptions authOptions) 
{ 
    var certificate = new X509Certificate2(Convert.FromBase64String(authOptions.CertificateKey), authOptions.CertificateSecret); 

    DateTime now = DateTime.UtcNow; 
    var claimset = new 
    { 
     iss = authOptions.Issuer, 
     scope = "https://www.googleapis.com/auth/plus.me", 
     aud = authOptions.Audience, 
     iat = ((int)now.Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture), 
     exp = ((int)now.AddMinutes(55).Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture) 
    }; 

    // header 
    var header = new { typ = "JWT", alg = "RS256" }; 

    // encoded header 
    var headerSerialized = JsonConvert.SerializeObject(header); 
    var headerBytes = Encoding.UTF8.GetBytes(headerSerialized); 
    var headerEncoded = TextEncodings.Base64Url.Encode(headerBytes); 

    // encoded claimset 
    var claimsetSerialized = JsonConvert.SerializeObject(claimset); 
    var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized); 
    var claimsetEncoded = TextEncodings.Base64Url.Encode(claimsetBytes); 

    // input 
    var input = String.Join(".", headerEncoded, claimsetEncoded); 
    var inputBytes = Encoding.UTF8.GetBytes(input); 

    // signiture 
    var rsa = (RSACryptoServiceProvider)certificate.PrivateKey; 
    var cspParam = new CspParameters 
    { 
     KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName, 
     KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2 
    }; 
    var cryptoServiceProvider = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false }; 
    var signatureBytes = cryptoServiceProvider.SignData(inputBytes, "SHA256"); 
    var signatureEncoded = TextEncodings.Base64Url.Encode(signatureBytes); 

    // jwt 
    return String.Join(".", headerEncoded, claimsetEncoded, signatureEncoded); 
} 
+0

ti dispiacerebbe condividere cosa è GoogleAuthOptions? – DaImTo

+0

@DaImTo: certo, eccolo https://github.com/abatishchev/ab/blob/master/Ab/Configuration/GoogleAuthOptions.cs – abatishchev

+0

Grazie, ci sto lavorando da quasi una settimana. Sto vicino ricevendo invalid_grant. – DaImTo

3

ho dovuto modificare il codice di @ abatishchev un po '. In caso contrario, ha avuto problemi a generare un certificato quando è stato distribuito in ambienti non di sviluppo.

Il problema era duplice. Se il certificato non è stato contrassegnato come esportabile, genererebbe un'eccezione dicendo qualcosa come "il keyset non esiste". Succederebbe solo sui server, non a livello locale, quindi sospetto che le versioni server di Windows siano più restrittive.

Inoltre, genera un'eccezione di crittografia relativa a un problema di attendibilità del computer poiché il certificato è stato creato in un set di chiavi utente. I nostri pool di applicazioni sono stati impostati per non importare il profilo utente nelle opzioni avanzate, che potreste fare. Ma non era un'opzione per noi a causa di problemi di compatibilità con altre app. L'impostazione del certificato da creare nel set di chiavi della macchina riduce il problema.

Le 2 sezioni modificate sono contrassegnate con commenti.

private static async Task<string> GetAuthorizationToken(GoogleAuthOptions authOptions) 
{ 
    string jwt = CreateJwt(authOptions); 

    var dic = new Dictionary<string, string> 
    { 
     { "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer" }, 
     { "assertion", jwt } 
    }; 
    var content = new FormUrlEncodedContent(dic); 

    var httpClient = new HttpClient { BaseAddress = new Uri("https://accounts.google.com") }; 
    var response = await httpClient.PostAsync("/o/oauth2/token", content); 
    response.EnsureSuccessStatusCode(); 

    dynamic dyn = await response.Content.ReadAsAsync<dynamic>(); 
    return dyn.access_token; 
} 

private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 

private static string CreateJwt(GoogleAuthOptions authOptions) 
{ 
    /* changed */ 
    const X509KeyStorageFlags certificateFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable; 

    var certificate = new X509Certificate2(Convert.FromBase64String(authOptions.CertificateKey), authOptions.CertificateSecret, certificateFlags); 
    /* end of change */ 

    DateTime now = DateTime.UtcNow; 
    var claimset = new 
    { 
     iss = authOptions.Issuer, 
     scope = "https://www.googleapis.com/auth/plus.me", 
     aud = authOptions.Audience, 
     iat = ((int)now.Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture), 
     exp = ((int)now.AddMinutes(55).Subtract(UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture) 
    }; 

    // header 
    var header = new { typ = "JWT", alg = "RS256" }; 

    // encoded header 
    var headerSerialized = JsonConvert.SerializeObject(header); 
    var headerBytes = Encoding.UTF8.GetBytes(headerSerialized); 
    var headerEncoded = TextEncodings.Base64Url.Encode(headerBytes); 

    // encoded claimset 
    var claimsetSerialized = JsonConvert.SerializeObject(claimset); 
    var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized); 
    var claimsetEncoded = TextEncodings.Base64Url.Encode(claimsetBytes); 

    // input 
    var input = String.Join(".", headerEncoded, claimsetEncoded); 
    var inputBytes = Encoding.UTF8.GetBytes(input); 

    // signiture 
    var rsa = (RSACryptoServiceProvider)certificate.PrivateKey; 
    var cspParam = new CspParameters 
    { 
     KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName, 
     /* changed */ 
     KeyNumber = (int) KeyNumber.Exchange,  
     Flags = CspProviderFlags.UseMachineKeyStore 
     /* end of change */ 
    }; 
    var cryptoServiceProvider = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false }; 
    var signatureBytes = cryptoServiceProvider.SignData(inputBytes, "SHA256"); 
    var signatureEncoded = TextEncodings.Base64Url.Encode(signatureBytes); 

    // jwt 
    return String.Join(".", headerEncoded, claimsetEncoded, signatureEncoded); 
} 
+0

hey, puoi spiegare i problemi che stai affrontando? – abatishchev

+0

e se vuoi posso semplicemente unire le tue modifiche alla mia risposta. – abatishchev

+0

Ho modificato il post per fornire maggiori dettagli sui problemi. Probabilmente hanno a che fare con la configurazione del server. Quindi manterrò il mio post separato in modo che possa aiutare con le persone che hanno questi problemi specifici. –

4

E 'stato un po' che questa domanda è stato chiesto, ma penso che per le persone in futuro a venire in questa pagina, può essere utile sapere che è morto facile da ottenere gli stessi risultati in poche righe di codice con l'API .NET di Google Auth (il cui NuGet è disponibile qui: Google.Apis.Auth

using System.Security.Cryptography.X509Certificates; 
using System.Threading; 
using System.Threading.Tasks; 
using Google.Apis.Auth.OAuth2; 

namespace GoogleTest 
{ 
    public class GoogleOAuth2 
    { 
     /// <summary> 
     /// Authorization scope for our requests 
     /// </summary> 
     private readonly string _defaultScope; 

     /// <summary> 
     /// Service account will be of the form [email protected] 
     /// </summary> 
     private readonly string _serviceAccount; 

     /// <summary> 
     /// Set this to the full path to your service account private key file. 
     /// </summary> 
     private readonly string _certificateFile; 

     public GoogleOAuth2(string defaultScope, string serviceAccount, string certificateFile) 
     { 
      _defaultScope = defaultScope; 
      _serviceAccount = serviceAccount; 
      _certificateFile = certificateFile; 
     } 

     /// <summary> 
     /// Access Token returned by Google Token Server 
     /// </summary> 
     public string AccessToken { get; set; } 

     public async Task<bool> RequestAccessTokenAsync() 
     { 
      var certificate = new X509Certificate2(_certificateFile, "notasecret", X509KeyStorageFlags.Exportable); 
      var serviceAccountCredential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(_serviceAccount) 
      { 
       Scopes = new[] { _defaultScope } 
      }.FromCertificate(certificate)); 

      var status = await serviceAccountCredential.RequestAccessTokenAsync(CancellationToken.None); 
      if (status) 
       AccessToken = serviceAccountCredential.Token.AccessToken; 
      return status; 
     } 
    } 
} 

per ottenere il token di accesso, è sufficiente chiamare il metodo RequestAccessTokenAsync e se il risultato è successo, hai la tua token nel Proprietà AccessToken

Si noti che questa implementazione presuppone che nella console degli sviluppatori sia stata esportata la propria chiave privata come file .P12.

Spero che questa risposta possa essere d'aiuto.

+0

provato questo codice, ma ottengo sempre lo stato come falso :-(qualche idea? – Vikram