使用 Azure Key Vault 签署 JWT 令牌
Sign JWT token using Azure Key Vault
我正在使用私钥签署 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,并粘贴 public 密钥 - 它表示签名已验证...
令牌也适用于 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.Keys
和 Azure.Identity
nuget 包)
这行不通。令牌的前两部分 - 即。 header 和 payload 与有效的 JWT 相同。唯一不同的是最后的签名。
我没主意了!请注意,这与 密切相关,其中的答案似乎表明我所做的应该是正确的。
您的代码大部分是正确的,但您应该使用 Encoding.UTF8
或 Encoding.ASCII
(因为 base64url 字符都是有效的 ASCII,并且您消除了任何 BOM 问题)来获取字节 headerAndPayload
.
我能够让它工作,发现 https://jwt.io 说您可以粘贴 public 密钥或证书时相当含糊。它必须是 PEM 编码的,如果发布 RSA public 密钥,则必须使用不太常见的“BEGIN RSA PUBLIC KEY”标签,而不是更常见的“BEGIN PUBLIC 键".
我尝试了一些应该都有效的方法,当我发现使用来自 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}");
我正在使用私钥签署 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,并粘贴 public 密钥 - 它表示签名已验证...
令牌也适用于 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.Keys
和 Azure.Identity
nuget 包)
这行不通。令牌的前两部分 - 即。 header 和 payload 与有效的 JWT 相同。唯一不同的是最后的签名。
我没主意了!请注意,这与
您的代码大部分是正确的,但您应该使用 Encoding.UTF8
或 Encoding.ASCII
(因为 base64url 字符都是有效的 ASCII,并且您消除了任何 BOM 问题)来获取字节 headerAndPayload
.
我能够让它工作,发现 https://jwt.io 说您可以粘贴 public 密钥或证书时相当含糊。它必须是 PEM 编码的,如果发布 RSA public 密钥,则必须使用不太常见的“BEGIN RSA PUBLIC KEY”标签,而不是更常见的“BEGIN PUBLIC 键".
我尝试了一些应该都有效的方法,当我发现使用来自 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}");