簡體   English   中英

如何使用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?

我正在嘗試創建一個JWT來使用System.IdentityModel.Tokens.JwtGoogle文檔中描述的服務帳戶進行授權。 我有以下代碼:

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獨家支持的算法?

自從提出這個問題以來已經有一段時間了,但我認為對於未來的人來說,可能值得知道,使用.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
    {
        /// <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文件。

希望這個答案會有所幫助。

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

我不得不稍微修改@ abatishchev的代碼。 否則,在部署到非開發環境時生成證書時會出現問題。

問題是雙重的。 如果證書未被標記為可導出,則會拋出異常,例如“keyset不存在”。 它只會發生在服務器上,而不是本地,所以我懷疑Windows的服務器版本更具限制性。

此外,它會引發有關計算機信任問題的加密例外,因為證書是在用戶密鑰集中創建的。 我們的應用程序池設置為不在高級選項中導入用戶配置文件,您可以這樣做。 但由於與其他應用程序的兼容性問題,它不適合我們。 設置要在計算機密鑰集中創建的證書可以緩解此問題。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM