10

I began using Azure Keyvault to store private keys for my application.

I have a use case where I need to sign a JWT token with an RSA private key.

When I had the private key in my application memory, it was easy, I would just do that

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

Now that I began to use Azure Keyvault, I want to see if it's possible to sign JWT tokens via the KeyVaultClient.SignAsync method.

Something along the lines of

KeyVaultClient client = ...;
var token = new JwtSecurityToken(
                issuer,
                ...,
                claims,
                ...,
                ...);
var tokenString = client.SignAsync(myKeyIdentifier, token);
user10962730
  • 979
  • 1
  • 7
  • 15

5 Answers5

11

First, a JWT token consists of three parts: Header, Payload and Signature. All of them are Base64UrlEncoded.

You can get the signature as following:

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

So, you need to generate the header and payload, combine them by dot, compute the hash, and then you can get the signature.

Here is a sample for your reference:

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)

The official SDK documentation for SignAsync

Wiki for JWT

Jack Jia
  • 5,268
  • 1
  • 12
  • 14
11

I ended up using Jack Jia's answer

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

I found another solution, which I didn't like as much but it "integrates" better with the JWT libraries.

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

Turns out that there is a library Microsoft.IdentityModel.KeyVaultExtensions with extensions to SecurityToken and ICryptoProvider which support KeyVault.

My problems with it are

  1. I can't reuse an existing instance of KeyVaultClient with this solution.
  2. It's blocking (Behind the scenes, it calls .GetAwaiter().GetResult() on KeyVaultClient.SignAsync
user10962730
  • 979
  • 1
  • 7
  • 15
0

In case anyone need's offline verification. I used user10962730's answer to generate the token and the following for verifying it offline:

While online, our client will retrieve the public information from our API. RSA Modulus and Exponent will be transfered Base64Url encoded instead of a byte array

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

Then when the client needs to verify a 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);
knmr
  • 11
  • 3
0

Do be aware that if you let Azure Key Vault sign your tokens, then there's a rate-limiting factor that you need to be aware of. I think its better to download the private key from AKV and then sign my tokens locally.

Read more at this link

Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • 1
    That's a great point, but "better" depends on several factors. If the goal is to keep the private key secret, then the better approach might be to throttle your own clients in response to 429 responses from Key Vault. You could also try using a circuit breaker or high-water-mark pattern to implement some back-pressure where you send your own 429 responses before making an actual request to Key Vault if there have been any recent 429 responses from Key Vault. – Lars Kemmann Dec 29 '20 at 23:34
0

For anyone looking for a less permissive approach (that is to say, not using KeyVaultClient, and using the new Azure APIs):

https://learn.microsoft.com/en-us/dotnet/api/azure.security.keyvault.keys.cryptography.cryptographyclient?view=azure-dotnet will allow you to call a method "SignDataAsync" that accepts the SignatureAlgorithm and your data (in byte[] or Stream form) and a cancellation token. It returns a SignatureResult which can be used to retrieve the actual signature as byte[] and various other information.

SignDataAsync would here be called on the "."-concatenated base64url-encoded header and payload (as described in rfc7515).