【问题标题】:Sign JWT token using Azure Key Vault使用 Azure Key Vault 签署 JWT 令牌
【发布时间】:2021-11-06 01:36:17
【问题描述】:

我正在使用私钥签署 JWT 令牌,它按预期工作。但是,我想利用 Azure Key Vault 为我进行签名,这样私钥就不会离开 KeyVault。我正在努力让它工作,但不知道为什么。

这是使用 KeyVault 并且确实工作的代码...

var handler = new JwtSecurityTokenHandler();

var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds();

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Iss, clientId),
    new Claim(JwtRegisteredClaimNames.Sub, integrationUser),
    new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
    new Claim(JwtRegisteredClaimNames.Exp, expiryTime.ToString()),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Add JTI for additional security against replay attacks
};

var privateKey = File.ReadAllText(@"selfsigned.key")
    .Replace("-----BEGIN PRIVATE KEY-----", "")
    .Replace("-----END PRIVATE KEY-----", "");

var privateKeyRaw = Convert.FromBase64String(privateKey);

var provider = new RSACryptoServiceProvider();
provider.ImportPkcs8PrivateKey(new ReadOnlySpan<byte>(privateKeyRaw), out _);
var rsaSecurityKey = new RsaSecurityKey(provider);

var token = new JwtSecurityToken
(
    new JwtHeader(new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256)),
    new JwtPayload(claims)
);

var token = handler.WriteToken(token);

这可行,如果我将 JWT 复制到 jwt.io,并粘贴公钥 - 它表示签名已验证...

令牌也适用于我正在调用的 API。

但是,如果使用 KeyVault 签名...

var handler = new JwtSecurityTokenHandler();

var expiryTime = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds();

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Iss, clientId),
    new Claim(JwtRegisteredClaimNames.Sub, integrationUser),
    new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
    new Claim(JwtRegisteredClaimNames.Exp, expiryTime.ToString()),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) // Add JTI for additional security against replay attacks
};

var header = @"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonConvert.SerializeObject(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";

// Sign token

var credential = new InteractiveBrowserCredential();

var client = new KeyClient(vaultUri: new Uri(kvUri), credential);
var key = (KeyVaultKey)client.GetKey("dan-test");

var cryptoClient = new CryptographyClient(keyId: key.Id, credential);

var digest = new SHA256CryptoServiceProvider().ComputeHash(Encoding.Unicode.GetBytes(headerAndPayload));
var signature = await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest);

var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature.Signature)}";

(使用 Azure.Security.KeyVault.KeysAzure.Identity nuget 包)

这不起作用。令牌的前两部分 - 即。标头和有效负载与有效的 JWT 相同。唯一不同的是最后的签名。

我没有想法!请注意,这与this * question 密切相关,其中的答案似乎表明我正在做的事情应该是正确的。

【问题讨论】:

标签: c# .net azure jwt azure-keyvault


【解决方案1】:

您的代码大部分是正确的,但您应该使用 Encoding.UTF8Encoding.ASCII(因为 base64url 字符都是有效的 ASCII 并且您消除了任何 BOM 问题)来获取 headerAndPayload 的字节。

我能够让它工作,并发现https://jwt.io 在说您可以粘贴公钥或证书时相当模糊。它必须是 PEM 编码的,如果发布 RSA 公钥,您必须使用不太常见的“BEGIN RSA PUBLIC KEY”标签,而不是更常见的“BEGIN PUBLIC KEY”。

我尝试了一些本来应该可行的方法,当我发现使用 Key Vault 中的证书对“BEGIN CERTIFICATE”有效时,我又尝试“BEGIN PUBLIC KEY”。直到一时兴起,当我将其更改为“BEGIN RSA PUBLIC KEY”时,JWT 才成功验证。

下面是我尝试使用证书 URI 的代码:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.IdentityModel.Tokens;

var arg = args.Length > 0 ? args[0] : throw new Exception("Key Vault key URI required");
var uri = new Uri(arg, UriKind.Absolute);

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Iss, Guid.NewGuid().ToString()),
    new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
    new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds().ToString()),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};

var header = @"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonSerializer.Serialize(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";

var id = new KeyVaultKeyIdentifier(uri);
var credential = new DefaultAzureCredential();

var certClient = new CertificateClient(id.VaultUri, credential);
KeyVaultCertificate cert = await certClient.GetCertificateAsync(id.Name);
using X509Certificate2 pfx = await certClient.DownloadCertificateAsync(id.Name, id.Version);

var pem = PemEncoding.Write("CERTIFICATE".AsSpan(), pfx.RawData);
Console.WriteLine($"Certificate (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();

using var rsaKey = pfx.GetRSAPublicKey();
var pubkey = rsaKey.ExportRSAPublicKey();
pem = PemEncoding.Write("RSA PUBLIC KEY".AsSpan(), pubkey.AsSpan());
Console.WriteLine($"Public key (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();

var cryptoClient = new CryptographyClient(cert.KeyId, credential);

using var sha256 = SHA256.Create();
var digest = sha256.ComputeHash(Encoding.ASCII.GetBytes(headerAndPayload));
var signature = (await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest)).Signature;

var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature)}";
Console.WriteLine($"JWT:\n\n{token}");

如果只使用一个密钥,以下应该可以工作:

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Azure.Identity;
using Azure.Security.KeyVault.Keys;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.IdentityModel.Tokens;

var arg = args.Length > 0 ? args[0] : throw new Exception("Key Vault key URI required");
var uri = new Uri(arg, UriKind.Absolute);

var claims = new[]
{
    new Claim(JwtRegisteredClaimNames.Iss, Guid.NewGuid().ToString()),
    new Claim(JwtRegisteredClaimNames.Aud, "https://test.example.com"),
    new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.Now.AddMinutes(10).ToUnixTimeSeconds().ToString()),
    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};

var header = @"{""alg"":""RS256"",""typ"":""JWT""}";
var payload = JsonSerializer.Serialize(new JwtPayload(claims));
var headerAndPayload = $"{Base64UrlEncoder.Encode(header)}.{Base64UrlEncoder.Encode(payload)}";

var id = new KeyVaultKeyIdentifier(uri);
var credential = new DefaultAzureCredential();

var keyClient = new KeyClient(id.VaultUri, credential);
KeyVaultKey key = await keyClient.GetKeyAsync(id.Name, id.Version);

using var rsaKey = key.Key.ToRSA();
var pubkey = rsaKey.ExportRSAPublicKey();
var pem = PemEncoding.Write("RSA PUBLIC KEY".AsSpan(), pubkey.AsSpan());
Console.WriteLine($"Public key (PEM):\n");
Console.WriteLine(pem);
Console.WriteLine();

var cryptoClient = new CryptographyClient(key.Id, credential);

using var sha256 = SHA256.Create();
var digest = sha256.ComputeHash(Encoding.ASCII.GetBytes(headerAndPayload));
var signature = (await cryptoClient.SignAsync(SignatureAlgorithm.RS256, digest)).Signature;

var token = $"{headerAndPayload}.{Base64UrlEncoder.Encode(signature)}";
Console.WriteLine($"JWT:\n\n{token}");

【讨论】:

  • 太棒了——谢谢! ? 只是从 Encoding.Unicode. 更改为 Encoding.ASCII. 为我修复了它。令人惊讶的是,一个单词怎么会引起这么多问题?
  • 我们如何使用私钥而不是公钥来签署 JWT?
  • 签名始终使用私钥完成,在此示例中由 Key Vault 完成,默认情况下不公开私钥。