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

Posted

技术标签:

【中文标题】如何使用 System.IdentityModel.Tokens.Jwt 生成具有 Google OAuth2 兼容算法 RSA SHA-256 的 JWT?【英文标题】:How to produce JWT with Google OAuth2 compatible algorithm RSA SHA-256 using System.IdentityModel.Tokens.Jwt? 【发布时间】:2014-12-16 04:55:47 【问题描述】:

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

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" 

虽然 Google 明确声明它支持 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.

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

【问题讨论】:

也许尝试使用签名和摘要算法的内置常量? (msdn.microsoft.com/en-us/library/…) @Jeff:嘿,抱歉,错过了关于您的评论的通知。好点子。但不幸的是仍然无法正常工作。 【参考方案1】:

这个问题被问到已经有一段时间了,但我认为对于未来访问此页面的人来说,可能值得知道的是,使用 .NET Google 只需几行代码就可以很容易地获得相同的结果身份验证 API(其 nuget 可在此处获得: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 nnnnnnn@developer.gserviceaccount.com
        /// </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 ,如果结果成功,您将在 AccessToken 属性中获得令牌。

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

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

【讨论】:

试过这段代码,但我总是得到错误的状态:-(有什么想法吗? 这段代码 sn-p 很好用。如果要使用 FCM 私钥,请将 serviceAccountCredential 实例化更改为: var serviceAccountCredential = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(_serviceAccount) Scopes = new[] _defaultScope .FromPrivateKey(_privateKey));其中 _privateKey 是 google-services.json 文件中的“private_key”值。【参考方案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)

    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);

【讨论】:

@DaImTo: 当然,这里是github.com/abatishchev/ab/blob/master/Ab/Configuration/… 谢谢我已经为此工作了将近一周。我即将获得 invalid_grant。【参考方案3】:

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

问题有两个方面。如果证书没有被标记为可导出,它会抛出一个异常,比如“keyset 不存在”。它只会在服务器上发生,而不是在本地发生,所以我怀疑 Windows 的服务器版本限制性更强。

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

2 个更改的部分用 cmets 标记。

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);

【讨论】:

嘿,你能解释一下你面临的问题吗? 如果您愿意,我可以将您的更改合并到我的答案中。 我编辑了帖子以提供有关问题的更多详细信息。它们可能与服务器配置有关。所以我会把我的帖子分开,这样它可以帮助有这些特定问题的人。 CSP 参数更改让我很开心。非常感谢!

以上是关于如何使用 System.IdentityModel.Tokens.Jwt 生成具有 Google OAuth2 兼容算法 RSA SHA-256 的 JWT?的主要内容,如果未能解决你的问题,请参考以下文章

我如何在 ASP.Net Core 2.1 mvc 应用程序中包含 System.Identitymodel 4.0

使用 Microsoft System.IdentityModel.Tokens.Jwt 在 Asp.net WebApi 中实现 JWT 身份验证

System.IdentityModel.Tokens 和 Microsoft.IdentityModel.Tokens 有啥区别?我应该在 ASP.NET Core 应用程序中使用哪一个?

System.IdentityModel.Tokens.JwtSecurityToken 自定义属性

使用 System.IdentityModel.Tokens.Jwt 使用 RS512 验证 JWT 签名

如何使用ConfigurationManager? (Microsoft.IdentityModel.Protocols)