Identityserver4: Q: Token validation with Azure KeyVault

Created on 18 Sep 2017  路  14Comments  路  Source: IdentityServer/IdentityServer4

  • [X ] I read and understood how to enable logging

I've managed to get Azure KeyVault connected to the identity server4 service and signing tokens for us via DI (very simple to do btw, thanks for that). However, I'm having some difficulty around validation.

The exception that is being thrown:

Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectMiddleware: Error: Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='.

I've injected IValidationKeysStore into the service:
services.AddTransient<IValidationKeysStore, KeyVaultValidationKeysStore>();

Here is the implementation:

public async Task<IEnumerable<SecurityKey>> GetValidationKeysAsync()
{
    var keyVaultClient = new KeyVaultClient(authentication.KeyVaultAuthenticationCallback);
    var keyVault = string.Format(KeyVaultConstants.KeyVaultUrlFormat, keyVaultSettings.VaultName);
    var keyVaultKeys = await keyVaultClient.GetKeysAsync(keyVault);
    var keys = new List<RsaSecurityKey>();
    foreach (var keyVaultKey in keyVaultKeys)
    {
        /** GetKeysAsync only returns meta data -- get actual key */
        var keyBundle = await keyVaultClient.GetKeyAsync(keyVaultKey.Identifier.Identifier);
        /** KeyVault keys' Kid is a url -- strip down to id */
        var kid = keyBundle.Key.Kid.Substring(keyBundle.Key.Kid.LastIndexOf('/') + 1);
        /** KeyVault keys' Exponent and Modulus are in base64 */
        var paramters = new RSAParameters
        {
            Exponent = keyBundle.Key.E,
            Modulus = keyBundle.Key.N,
        };
        var key = new RsaSecurityKey(paramters)
        {
            KeyId = kid,
        };
        keys.Add(key);
    }
    return keys;
}

The keys are getting picked up. I've verified when I hit the /identity/.well-known/openid-configuration/jwks endpoint the keys are returned:

{
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "33371ac06dc341c0a33570afa360a8d5",
            "e": "AQAB",
            "n": "0mJrfNkwptknurikY_czpq2Q3oVb4Y9vYj7zXt7H0gsfyyHHSeY0-DmJBKhSlt99LZ2YMKo1xFery4s0iS-yAWhfJ5JUCjVZnIfMk9lAOupyCbknwOU4AibGOC_4VURgjHOfSVZ_cVNrC9h6fgcgTMkxTm-XtLvdfmXfFxHk-OOtN_saN7wrQU7jRgDT4Aare_uJmuOcbxh73Ts9d6K15FWu7amflc5QgWnOGfAnJr151ITA0GIqWVlWsz8LHWL2INI0NnT3sYOGIQ0OwxCdgZIpzRmXZRw0jerOPevJyxx_1t6N4lSx95aNt2xuXM-W7zu40MDh3CstK4fqzcus-w"
        }
    ]
}

I've manually verified a token's signature can be validated with the public key by grabbing the details from the debugger and dropping them in a small console app I wrote up.

I'm wondering if there is something I'm missing as far as DI goes? Any help with this would be appreciated.

Relevant parts of the log file

Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectMiddleware: Error: Unable to read the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectMiddleware: Error: Exception occurred while processing message.

Microsoft.IdentityModel.Tokens.SecurityTokenException: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='."
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateToken(String idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, JwtSecurityToken& jwt)
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.<HandleRemoteAuthenticateAsync>d__20.MoveNext()
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectMiddleware: Information: Error from RemoteAuthentication: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='.".
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request

System.AggregateException: Unhandled remote failure. ---> Microsoft.IdentityModel.Tokens.SecurityTokenException: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='."
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateToken(String idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, JwtSecurityToken& jwt)
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.<HandleRemoteAuthenticateAsync>d__20.MoveNext()
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.<HandleRemoteCallbackAsync>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.<HandleRequestAsync>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.<HandleRequestAsync>d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware`1.<Invoke>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.VisualStudio.Web.BrowserLink.Runtime.BrowserLinkMiddleware.<ExecuteWithFilter>d__7.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()
---> (Inner Exception #0) Microsoft.IdentityModel.Tokens.SecurityTokenException: Unable to validate the 'id_token', no suitable ISecurityTokenValidator was found for: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjMzMzcxYWMwNmRjMzQxYzBhMzM1NzBhZmEzNjBhOGQ1IiwidHlwIjoiSldUIn0.eyJuYmYiOjE1MDU3NDEzNzcsImV4cCI6MTUwNTc0MTY3NywiaXNzIjoiaHR0cDovL2xvY2FsLmFuZGkuY29tL2lkZW50aXR5IiwiYXVkIjoiYW5kaS5nYWxsZXJ5LmNsaWVudCIsIm5vbmNlIjoiNjM2NDEzMzgxNzcwNTEwNTU1Lk0yWmpObU00TnpNdE56a3pPQzAwTnpOa0xUbGxNalF0TXpReVpHVTJNV0ppWW1JeU5qYzNZbVkwWVRZdFl6VTRaUzAwTkRFd0xXSTFNREF0WW1JeVltWTRPRFZtT0dFMiIsImlhdCI6MTUwNTc0MTM3Nywic2lkIjoiMzNiM2ZmNzE2ZjZkZDg1NTdjMWI4YTUzYmQ2ZjBjZmEiLCJzdWIiOiIyYTI1ZDFlYi1hZTdkLTQxMjctYWY0OC1kNzYzNmNkOTkzZjUiLCJhdXRoX3RpbWUiOjE1MDU3NDEzNzMsImlkcCI6ImxvY2FsIiwibmFtZSI6ImNsYXdyZW5jZUBwcmVjaXNpb25sZW5kZXIuY29tIiwiZW1haWwiOiJjbGF3cmVuY2VAcHJlY2lzaW9ubGVuZGVyLmNvbSIsImFuZGkub3JnSWQiOiI5OWI5NDEyYi03MGZkLTQyMjEtODQzMS1kMWIzOTM0ODg5ZWMiLCJhbXIiOlsicHdkIl19.GDlw0vEShMHmjG19QeQE0MYESlePrzBnfUxHtQZ2vfbbPi8WuUjr2VGFrxDlzkqCwGOq8j7FZQ83OlfrMk8aZYd5J8ZsVKwruoHPdmGRpnjj2wv/5Ujk7545rdCxwT61NesIa43hFf9qxI7IRr5N6+TQFVjLaGZpmcpT/c5lKhgqPstMpcoZeObQYPv+VSoDnGRXRd2aHn4fhZQz9OMw1ZWfGNa77yw+UD9YL6nWOEWhpdDodbn7+QfUK6k148BmLYzXo/2BPvW/NmSNsjUBEPHdHkAt8aQBxY6EKKtV7/Q6r/dW66yDNWxLrU2DR4SmPnOzsasPOFQ1PYXzbaf7Hg=='."
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateToken(String idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, JwtSecurityToken& jwt)
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.<HandleRemoteAuthenticateAsync>d__20.MoveNext()<---
question

Most helpful comment

Sure, here is the working code. I'm simply sub-classing from DefaultTokenCreationService and overriding CreateJwtAsync and CreateHeaderAsync

protected override Task<string> CreateJwtAsync(JwtSecurityToken jwt)
{
    var rawDataBytes = System.Text.Encoding.UTF8.GetBytes(jwt.EncodedHeader + "." + jwt.EncodedPayload);

    /** KeyVault keys' KeyIdentifierFormat: https://{vaultname}.vault.azure.net/keys/{keyname} */
    var keyIdentifier = string.Format(KeyVaultConstants.KeyIdentifierFormat, settings.VaultName, settings.KeyName);
    var signatureProvider = new KeyVaultSignatureProvider(keyIdentifier, JsonWebKeySignatureAlgorithm.RS256, authentication.KeyVaultAuthenticationCallback);

    var rawSignature = await Task.Run(() => Base64UrlEncoder.Encode(signatureProvider.Sign(rawDataBytes))).ConfigureAwait(false);

    return jwt.EncodedHeader + "." + jwt.EncodedPayload + "." + rawSignature;
}

protected override async Task<JwtHeader> CreateHeaderAsync(Token token)
{
    var credentials = await GetSigningCredentialsAsync();
    var header = new JwtHeader(credentials);
    return header;
}

private async Task<SigningCredentials> GetSigningCredentialsAsync()
{
    var jwk = await publicKeyProvider.GetSigningCredentialsAsync();
    var parameters = new RSAParameters
    {
        Exponent = Base64UrlEncoder.DecodeBytes(jwk.E),
        Modulus = Base64UrlEncoder.DecodeBytes(jwk.N)
    };
    var securityKey = new RsaSecurityKey(parameters)
    {
        KeyId = jwk.Kid,
    };

    return new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
}

Code for getting public key for header usage:

public async Task<JsonWebKey> GetSigningCredentialsAsync()
{
    var keyVaultClient = new KeyVaultClient(authentication.KeyVaultAuthenticationCallback);
    /** KeyVault url format https://{vaultname}.vault.azure.net */
    var keyVault = string.Format(KeyVaultConstants.KeyVaultUrlFormat, keyVaultSettings.VaultName);
    var singleKey = await keyVaultClient.GetKeyAsync(keyVault, keyVaultSettings.KeyName);
    /** KeyVault keys' Kid are a url -- extract just the id/version */
    var kid = singleKey.Key.Kid.Substring(singleKey.Key.Kid.LastIndexOf('/') + 1);
    var jsonString = JsonConvert.SerializeObject(singleKey.Key);
    return new JsonWebKey(jsonString)
    {
        Kid = kid
    };
}

All 14 comments

Can you check if the key id in the discovery document matches the key id in the JWT header?

Just verified by dropping the JWT into jwt.io:

{
  "alg": "RS256",
  "kid": "33371ac06dc341c0a33570afa360a8d5",
  "typ": "JWT"
}

Which matches the id in the jwks endpoint (see original post).

that's weird. maybe open an issue in the JWT handler repo?

Fixed. It was a problem with the way I was encoding the bytes returned from Azure KeyVault after signing the token. I was encoding with base64 _string_ instead of _url_ 馃槩

Thanks for the quick replies any way @leastprivilege Maybe this will help someone else some day.

Could you post your complete code?

Sure, here is the working code. I'm simply sub-classing from DefaultTokenCreationService and overriding CreateJwtAsync and CreateHeaderAsync

protected override Task<string> CreateJwtAsync(JwtSecurityToken jwt)
{
    var rawDataBytes = System.Text.Encoding.UTF8.GetBytes(jwt.EncodedHeader + "." + jwt.EncodedPayload);

    /** KeyVault keys' KeyIdentifierFormat: https://{vaultname}.vault.azure.net/keys/{keyname} */
    var keyIdentifier = string.Format(KeyVaultConstants.KeyIdentifierFormat, settings.VaultName, settings.KeyName);
    var signatureProvider = new KeyVaultSignatureProvider(keyIdentifier, JsonWebKeySignatureAlgorithm.RS256, authentication.KeyVaultAuthenticationCallback);

    var rawSignature = await Task.Run(() => Base64UrlEncoder.Encode(signatureProvider.Sign(rawDataBytes))).ConfigureAwait(false);

    return jwt.EncodedHeader + "." + jwt.EncodedPayload + "." + rawSignature;
}

protected override async Task<JwtHeader> CreateHeaderAsync(Token token)
{
    var credentials = await GetSigningCredentialsAsync();
    var header = new JwtHeader(credentials);
    return header;
}

private async Task<SigningCredentials> GetSigningCredentialsAsync()
{
    var jwk = await publicKeyProvider.GetSigningCredentialsAsync();
    var parameters = new RSAParameters
    {
        Exponent = Base64UrlEncoder.DecodeBytes(jwk.E),
        Modulus = Base64UrlEncoder.DecodeBytes(jwk.N)
    };
    var securityKey = new RsaSecurityKey(parameters)
    {
        KeyId = jwk.Kid,
    };

    return new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256);
}

Code for getting public key for header usage:

public async Task<JsonWebKey> GetSigningCredentialsAsync()
{
    var keyVaultClient = new KeyVaultClient(authentication.KeyVaultAuthenticationCallback);
    /** KeyVault url format https://{vaultname}.vault.azure.net */
    var keyVault = string.Format(KeyVaultConstants.KeyVaultUrlFormat, keyVaultSettings.VaultName);
    var singleKey = await keyVaultClient.GetKeyAsync(keyVault, keyVaultSettings.KeyName);
    /** KeyVault keys' Kid are a url -- extract just the id/version */
    var kid = singleKey.Key.Kid.Substring(singleKey.Key.Kid.LastIndexOf('/') + 1);
    var jsonString = JsonConvert.SerializeObject(singleKey.Key);
    return new JsonWebKey(jsonString)
    {
        Kid = kid
    };
}

@christopher-lawrence, out of curiosity -- how well does this perform in production?

@brockallen We are still in a pre-production state, so I can't actually answer you at this time. Internally, we have about 10-20 users that have not experienced any issues. KeyVault -- knocks on wood -- seems to be a fairly reliable resource within Azure for us so far.

I'll make a note to come back and update this post with some information once it's available.

Ok, thanks. I'm curious about reliability but also simply the latency using key vault for signing all tokens.

@brockallen Am i wrong when i read the code as its downloading the information to sign (and caching) so there is no latency for signing tokens.

I was under the impression they wanted to use the key vault HSM. But if they're just loading keys from keyvault at startup and treating keyvault as a key DB then that'd work.

When i read the code, they are using keyvault keys (where you can accesss the public part of the key) for signing.

The code is not caching (but it could).

I dont know if you can from HSM module also get public key part.

But anyway, looks interesting - need to try it out soon.

While you can theoretically put anything you want into the HSM, it is pointless to put the public key there as it will also be in the cert which (usually) needs to be in the cert store. That would be the place to get the public key. I ran some perf tests on the HSM for Microsoft at one time, the new HSMs are pretty good, but will never be as fast as running on the processor. You can go to the nCipher web site if you want throughput numbers.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings