2014-10-21 3 views
6

Google documentation에 설명 된대로 서비스 계정으로 승인 할 JWT를 만들려고합니다. System.IdentityModel.Tokens.Jwt을 사용하고 있습니다.System.IdentityModel.Tokens.Jwt를 사용하여 Google OAuth2 호환 알고리즘 RSA SHA-256으로 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); 

출력 : 나는 다음과 같은 코드가

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

구글이 명시 적으로 SHA-256 지원 상태 일 때 :

서비스 계정은 RSA SHA-256에 의존 알고리즘 및 JWT 토큰 형식

따르면 wtSecurityTokenHandler.InboundAlgorithmMap에 :

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

나는 예외 받고 있어요 :

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. 

는이 알고리즘을 지원하지 않습니다 마이크로 소프트 뜻을

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

그래서 내가 코드를 변경하는 경우 Google은 독점적으로 지원합니까?

+0

아마도 서명 및 다이제스트 알고리즘에 내장 된 상수를 사용해보십시오. (http://msdn.microsoft.com/en-us/library/system.identitymodel.tokens.securityalgorithms%28v=vs.110%29.aspx) –

+0

@Jeff : 안녕하세요, 죄송합니다. 사용자 의견에 대한 알림을 놓쳤습니다. 좋은 지적. 그러나 불행히도 여전히 작동하지 않습니다. – abatishchev

답변

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

GoogleAuthOptions을 공유 하시겠습니까? – DaImTo

+0

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

+0

입니다. 감사합니다. 저는 거의 일주일 동안이 작업을 해왔습니다. 나는 invalid_grant를 가까이 얻고있다. – DaImTo

3

@ abatishchev의 코드를 약간 수정해야했습니다. 그렇지 않으면 비 개발 환경에 배포 할 때 인증서를 생성하는 데 문제가있었습니다.

두 가지 문제가있었습니다. 인증서가 내보낼 수 있다고 플래그되지 않으면 "키 집합이 존재하지 않음"과 같은 예외를 throw합니다. 로컬 서버가 아닌 서버에서만 발생하므로 서버 버전의 Windows가 더 제한적인 것으로 의심됩니다.

또한 사용자 키 집합에 인증서가 만들어져 컴퓨터 신뢰 문제에 대한 암호화 예외가 발생합니다. 우리의 응용 프로그램 풀은 사용자가 할 수있는 고급 옵션에서 사용자 프로필을 가져 오지 않도록 설정되었습니다. 하지만 다른 앱과의 호환성 문제로 인해 Google이 선택할 수있는 옵션이 아닙니다. 컴퓨터 키 집합에 생성 할 인증서를 설정하면 문제가 완화됩니다.

변경된 섹션 2 개에는 주석이 표시됩니다. 이 질문에 질문을 받았다 이후

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

이봐, 네가 직면 한 문제를 설명해 주시겠습니까? – abatishchev

+0

그리고 원하는 경우 변경 사항을 내 대답으로 병합 할 수 있습니다. – abatishchev

+0

문제에 대한 자세한 정보를 제공하기 위해 게시물을 편집했습니다. 아마 서버 구성과 관련이 있습니다. 그래서 나는 그 특정 문제가있는 사람들을 도울 수 있도록 내 게시물을 별도로 보관할 것입니다. –

4

그것은 잠시왔다하지만이 페이지에서 오는 미래의 사람들을 위해, 그것은 몇 줄의 코드와 동일한 결과를 얻을 죽은 쉽게 것을 알고 가치가있을 수 있다고 생각 (그 nuget는 .NET 구글 인증 API는 여기에 있습니다 : 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; 
     } 
    } 
} 

이 액세스 토큰을 얻으려면, 당신은 단지 방법 RequestAccessTokenAsync 전화를해야하고 그 결과가 성공하면, 당신은 당신의 토큰을 가지고 액세스 토큰 속성

이 구현에서는 개발자 콘솔에서 개인 키를 .P12 파일로 내 보낸 것으로 가정합니다.

희망이 답변을 도울 것입니다.

+0

이 코드를 시도했지만 항상 상태가 false로 표시됩니다 .--(모든 아이디어? – Vikram