如何使用System.IdentityModel.Tokens.Jwt使用Google OAuth2兼容算法RSA SHA-256生成JWT?

我正在尝试创建一个JWT来使用System.IdentityModel.Tokens.Jwt在Google文档中描述的服务帐户进行授权。 我有以下代码:

byte[] key = Convert.FromBase64String("..."); var certificate = new X509Certificate2(key, "notasecret"); DateTime now = DateTime.UtcNow; TimeSpan span = now - UnixEpoch; Claim[] claims = { new Claim("iss", "email@developer.gserviceaccount.com"), 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 :

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

所以当我改变我的代码时:

 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. 

这是否意味着微软不支持Google独家支持的算法?

 private static async Task GetAuthorizationToken(GoogleAuthOptions authOptions) { string jwt = CreateJwt(authOptions); var dic = new Dictionary { { "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(); 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); } 

自从提出这个问题以来已经有一段时间了,但我认为对于未来的人来说,可能值得知道,使用.NET Google Auth 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 { ///  /// Authorization scope for our requests ///  private readonly string _defaultScope; ///  /// Service account will be of the form nnnnnnn@developer.gserviceaccount.com ///  private readonly string _serviceAccount; ///  /// Set this to the full path to your service account private key file. ///  private readonly string _certificateFile; public GoogleOAuth2(string defaultScope, string serviceAccount, string certificateFile) { _defaultScope = defaultScope; _serviceAccount = serviceAccount; _certificateFile = certificateFile; } ///  /// Access Token returned by Google Token Server ///  public string AccessToken { get; set; } public async Task 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方法,如果结果成功,您就可以在AccessToken属性中获得令牌。

请注意,此实现假定在开发人员控制台中,您已将私钥导出为.P12文件。

希望这个答案会有所帮助。

我不得不稍微修改@ abatishchev的代码。 否则,在部署到非开发环境时生成证书时会出现问题。

问题是双重的。 如果证书未被标记为可导出,则会抛出exception,例如“keyset不存在”。 它只会发生在服务器上,而不是本地,所以我怀疑Windows的服务器版本更具限制性。

此外,它会引发有关计算机信任问题的加密例外,因为证书是在用户密钥集中创建的。 我们的应用程序池设置为不在高级选项中导入用户配置文件,您可以这样做。 但由于与其他应用程序的兼容性问题,它不适合我们。 设置要在计算机密钥集中创建的证书可以缓解此问题。

2个更改的部分标有注释。

 private static async Task GetAuthorizationToken(GoogleAuthOptions authOptions) { string jwt = CreateJwt(authOptions); var dic = new Dictionary { { "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(); 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); }