Azure KeyVault - 签署 JWT 令牌

Azure KeyVault - Sign JWT Token

我开始使用 Azure Keyvault 来存储我的应用程序的私钥。

我有一个用例,我需要使用 RSA 私钥签署 JWT 令牌。

当我的应用程序内存中有私钥时,这很容易, 我会这样做

var token = new JwtSecurityToken(
                issuer,
                ...,
                claims,
                ...,
                ...,
                signingCredentials_PrivateKey);

现在我开始使用 Azure Keyvault,我想看看是否可以通过 KeyVaultClient.SignAsync 方法对 JWT 令牌进行签名。

类似于

KeyVaultClient client = ...;
var token = new JwtSecurityToken(
                issuer,
                ...,
                claims,
                ...,
                ...);
var tokenString = client.SignAsync(myKeyIdentifier, token);

首先,一个JWT token由三部分组成:Header、Payload和Signature。都是Base64UrlEncoded.

您可以获得如下签名:

HMAC-SHA256(
 base64urlEncoding(header) + '.' + base64urlEncoding(payload),
 secret
)

所以,你需要生成header和payload,用dot组合起来,计算hash,然后就可以得到签名了。

这里有一个示例供您参考:

var byteData = Encoding.Unicode.GetBytes(base64urlEncoding(header) + "." + base64urlEncoding(payload));
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var signature = await keyClient.SignAsync(keyIdentifier, "RS256", digest);
var token = base64urlEncoding(header) + "." + base64urlEncoding(payload) + "." + base64urlEncoding(signature)

SignAsync

的官方SDK文档

JWT

的 Wiki

我最终使用了 Jack Jia 的答案

var token = new JwtSecurityToken(
                issuer,
                appId,
                claims,
                signDate,
                expiryDate);

var header = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(new Dictionary<string, string>()
{
    { JwtHeaderParameterNames.Alg, "RS256" },
    { JwtHeaderParameterNames.Kid, "https://myvault.vault.azure.net/keys/mykey/keyid" },
    { JwtHeaderParameterNames.Typ, "JWT" }
}));
var byteData = Encoding.UTF8.GetBytes(header + "." + token.EncodedPayload);
var hasher = new SHA256CryptoServiceProvider();
var digest = hasher.ComputeHash(byteData);
var signature = await _keyVault.SignAsync("https://myvault.vault.azure.net/keys/mykey/keyid", "RS256", digest);

return $"{header}.{token.EncodedPayload}.{Base64UrlEncoder.Encode(signature.Result)}";

我找到了另一个解决方案,我不太喜欢它,但它 "integrates" 更适合 JWT 库。

var token = new JwtSecurityToken(
    issuer,
    appId,
    claims,
    signDate,
    expiryDate,
    new SigningCredentials(new KeyVaultSecurityKey("https://myvault.vault.azure.net/keys/mykey/keyid", new KeyVaultSecurityKey.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)), "RS256")
    {
        CryptoProviderFactory = new CryptoProviderFactory() { CustomCryptoProvider = new KeyVaultCryptoProvider() }
    });

var handler = new JwtSecurityTokenHandler();
return handler.WriteToken(token);

原来有一个库 Microsoft.IdentityModel.KeyVaultExtensions 扩展到 SecurityTokenICryptoProvider 支持 KeyVault。

我的问题是

  1. 我无法通过此解决方案重用 KeyVaultClient 的现有实例。
  2. 它正在阻塞(在幕后,它在 KeyVaultClient.SignAsync
  3. 上调用 .GetAwaiter().GetResult()

以防有人需要离线验证。我使用 user10962730 的答案生成令牌,并使用以下内容进行离线验证:

在线时,我们的客户端将从我们的 API 检索 public 信息。 RSA 模数和指数将被传输 Base64Url 编码而不是字节数组

var keyBundle = await _keyVault.GetKeyAsync("https://myvault.vault.azure.net/keys/mykey/keyid");
return new { n = keyBundle.N, e = keyBundle.E };

然后当客户端需要验证一个token时

string jwtToken = "[Header].[Payload].[Signature]";
var jwtParts = jwtToken.Split(".");
var rsa = new RSACryptoServiceProvider();
var p = new RSAParameters() { Modulus = Base64UrlEncoder.DecodeBytes(key.n), Exponent = Base64UrlEncoder.DecodeBytes(key.e) };
rsa.ImportParameters(p);
var dataToHash = Encoding.UTF8.GetBytes($"{jwtParts[0]}.{jwtParts[1]}");
byte[] digestBytes = SHA256.Create().ComputeHash(dataToHash);
var sigBytes = Base64UrlEncoder.DecodeBytes(jwtParts[2]);
var isVerified = rsa.VerifyHash(digestBytes, "Sha256", sigBytes);

请注意,如果您让 Azure Key Vault 对您的令牌进行签名,则需要注意一个 rate-limiting 因素。我觉得最好从AKV下载私钥然后在本地签名我的代币。

在此阅读更多内容 link

对于寻求宽松方法(也就是说,不使用 KeyVaultClient,而使用新的 Azure API)的任何人:

https://docs.microsoft.com/en-us/dotnet/api/azure.security.keyvault.keys.cryptography.cryptographyclient?view=azure-dotnet 将允许您调用方法“SignDataAsync”,该方法接受 SignatureAlgorithm 和您的数据(以 byte[] 或 Stream 形式)和取消令牌。它 returns 一个 SignatureResult,可用于检索实际签名作为 byte[] 和各种其他信息。

SignDataAsync 将在此处调用“.”-连接的 base64url 编码 header 和有效负载(如 rfc7515 中所述)。