Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Make TokenValidator extensible #1847

Closed
leastprivilege opened this issue Dec 11, 2017 · 23 comments
Closed

Make TokenValidator extensible #1847

leastprivilege opened this issue Dec 11, 2017 · 23 comments

Comments

@leastprivilege
Copy link
Member

to support other JWT libs and e.g. JWE

@bashiransari
Copy link

@leastprivilege thanks for opening this issue. I might be able to put sometime on this and create a PR in christmas period

@leastprivilege
Copy link
Member Author

I need to know the use cases first. What's yours?

@brockallen brockallen modified the milestones: 2.1, Future Dec 27, 2017
@bashiransari
Copy link

Sorry for the late reply,
We are implementing identity provider in an ecosystem with many legacy applications which we cannot have backchannel communication due to infrastructure limitations. We decided to go with implicit-flow with encrypted id token as we have some sensitive information in claims.

@leastprivilege
Copy link
Member Author

OK - that's fine. You can do that with a custom token creation service. But why would you need a custom token validator?

@leastprivilege
Copy link
Member Author

Oh I see - do you want to validate your encrypted id_tokens for signout?

@leastprivilege leastprivilege modified the milestones: Future, 2.1 Jan 3, 2018
@leastprivilege
Copy link
Member Author

Is this your scenario?

If you don't give me more information, we can't put it into the next release..

@leastprivilege
Copy link
Member Author

OK - since we didn't get a response. This will be postponed.

I think ultimately I want to split up the validators for id_token, access token and refresh token - and a shared JwtValidator that can be replaced independently.

@leastprivilege leastprivilege modified the milestones: 2.1, 2.2 Jan 5, 2018
@bashiransari
Copy link

Sorry for late reply.
No problem, I am using a work around for now, will wait for the 2.2 .

Thanks 🙏

@leastprivilege
Copy link
Member Author

You still haven't answered my question...

@bashiransari
Copy link

Oh, Sorry :-(
Yes, I need to read the idtokenhint in signout flow

@leastprivilege
Copy link
Member Author

What is your workaround today?

  • How do you encrypt the token?
  • How do you get the client id at validation time (before the token is decrypted)?

@bashiransari
Copy link

For encryption I am using custom token creation service to create encrypted token
For validation I have copied tokenvalidator source code and modified it to support decryption

@leastprivilege
Copy link
Member Author

Would you make that source code available so I can have a look?

@bashiransari
Copy link

Yes, I will share the code tomorrow

@leastprivilege leastprivilege modified the milestones: 2.2, 3.0 Feb 28, 2018
@jaredrsowers
Copy link

jaredrsowers commented May 2, 2018

I also need TokenValidator and PrivateKeyJwtSecretValidator to be extensible for the same reason.

I need to use JWE instead of JWT. The token creation is already there, just the validation is missing. In both classes by just adding something similar to a "protected virtual TokenValidationParameters GetTokenValidationParameters(trustedKeys, validIssuer, validAudience)" would probably be sufficient, but it is needed in both classes...

@leastprivilege
Copy link
Member Author

maybe @bashiransari can show us his code?

@leastprivilege
Copy link
Member Author

Can you post the code you are using for token creation? and also a prototype of how the validation would need to look like?

@jaredrsowers
Copy link

This is my WIP for generating the JWE by extending the DefaultTokenCreationService:

public interface IExtendedKeyMaterialService : IKeyMaterialService
{
    Task<EncryptingCredentials> GetEncryptingCredentialsAsync();
}

public class ExtendedKeyMaterialService : DefaultKeyMaterialService, IExtendedKeyMaterialService
{
    public ExtendedKeyMaterialService(IEnumerable<IValidationKeysStore> validationKeys, ISigningCredentialStore signingCredential = null)
    : base(validationKeys, signingCredential)
    {
    }

    public Task<EncryptingCredentials> GetEncryptingCredentialsAsync()
    {
        var key = Convert.FromBase64String("some static key");

        var jweSecurityKey = new SymmetricSecurityKey(key)
        {
            KeyId = "aes_key_id"
        };

        var encryptingCredentials = new EncryptingCredentials(jweSecurityKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);

        return Task.FromResult(encryptingCredentials);
    }
}

public class ExtendedTokenCreationService : DefaultTokenCreationService
{
    private readonly IExtendedKeyMaterialService _keys;

    public ExtendedTokenCreationService(ISystemClock clock, IExtendedKeyMaterialService keys, ILogger<ExtendedTokenCreationService> logger)
    : base(clock, keys, logger)
    {
        _keys = keys;
    }

    protected override async Task<string> CreateJwtAsync(JwtSecurityToken jwt)
    {
        var encryptingCredentials = await _keys.GetEncryptingCredentialsAsync();

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            EncryptingCredentials = encryptingCredentials,
            SigningCredentials = jwt.SigningCredentials,
            Subject = new ClaimsIdentity(jwt.Claims)
        };

        var handler = new JwtSecurityTokenHandler();

        var jwe = handler.CreateJwtSecurityToken(tokenDescriptor);

        return jwe.RawData ?? handler.WriteToken(jwe);
    }
}

Everything seem to be working properly in my testing.

@bashiransari
Copy link

Hi,
Sorry for late reply, I have customized TokenValidator.ValidateIdentityTokenAsync method to enable token validation in logout flow as well. In my case, because I have used a different encryption key for each client I needed clientId for token validation so, in log out, the client will append the client id to the 5 part token as will send it as id_token_hint parameter.
I have copied TokenValidator source and modified ValidateJwtAsync and GetClientIdFromJwt method as below,

        protected override string GetClientIdFromJwt(string token)
        {
            //in logout flow, id_token_hint might contain clientId as the last section of token
            //This is our custom interim solution to get clientId when id_token_hint is encrypted
            try
            {
                var tokenParts = token.Split('.');
                if (tokenParts.Length == 6)
                {
                    return Base64UrlEncoder.Decode(tokenParts[5]);
                }

                var jwt = new JwtSecurityToken(token);
                var clientId = jwt.Audiences.FirstOrDefault();

                return clientId;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Malformed JWT token: {exception}", ex.Message);
                return null;
            }
        }

        private async Task<TokenValidationResult> ValidateJwtAsync(string jwt, string audience, IEnumerable<SecurityKey> validationKeys, bool validateLifetime = true)
        {
            var handler = new JwtSecurityTokenHandler();
            handler.InboundClaimTypeMap.Clear();

            var parameters = new TokenValidationParameters
            {
                ValidIssuer = _context.HttpContext.GetIdentityServerIssuerUri(),
                IssuerSigningKeys = validationKeys,
                ValidateLifetime = validateLifetime,
                ValidAudience = audience
            };

            //setting jwe decryption key
            if (jwt.Count(p => p == '.') == 5)
            {
                //remove the clientId to have a standard jwe
                jwt = jwt.Substring(0, jwt.LastIndexOf(".", StringComparison.InvariantCulture));

                var client = await _clientService.GetClient(audience);
                var secret = client.GetIdTokenSharedSecret();

                parameters.TokenDecryptionKey =
                    await _keyService.GetDecryptionKeyAsync(GetDecryptingKeyId(jwt), secret);
            }
            //end setting jwe decryption key

            try
            {
                var id = handler.ValidateToken(jwt, parameters, out var _);

                // if access token contains an ID, log it
                var jwtId = id.FindFirst(JwtClaimTypes.JwtId);
                if (jwtId != null)
                {
                    _log.JwtId = jwtId.Value;
                }

                // load the client that belongs to the client_id claim
                Client client = null;
                var clientId = id.FindFirst(JwtClaimTypes.ClientId);
                if (clientId != null)
                {
                    client = await _clients.FindEnabledClientByIdAsync(clientId.Value);
                    if (client == null)
                    {
                        throw new InvalidOperationException("Client does not exist anymore.");
                    }
                }

                return new TokenValidationResult
                {
                    IsError = false,

                    Claims = id.Claims,
                    Client = client,
                    Jwt = jwt
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "JWT token validation error: {exception}", ex.Message);
                return Invalid(OidcConstants.ProtectedResourceErrors.InvalidToken);
            }
        }

This is not a perfect solution it would be better if the client sends its own clientId as a query string parameter but I didn't want to use HttpContextAccessor in TokenValidator directly, I think the caller classes like EndSessionRequestValidator (

var tokenValidationResult = await _tokenValidator.ValidateIdentityTokenAsync(idTokenHint, null, false);
) should send the parameters to the ValidateIdentityTokenAsync method.

@leastprivilege
Copy link
Member Author

the client will append the client id to the 5 part token as will send it as id_token_hint parameter.

I think there are better solution to this. Something we need to figure out first.

Given that this requires some more research, and we have other higher priority issues right now - we have no plans right now to support encryption built-in.

If your company needs that feature built-in, they can sponsor us and we can work out something.

@DureSameen
Copy link

please see my example implementations : https://github.com/DureSameen/IdentityServer4.JWE

@stale
Copy link

stale bot commented Jan 10, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Jan 10, 2020
@stale stale bot removed the wontfix label Jan 10, 2020
@brockallen brockallen modified the milestones: 4.0, Future Mar 26, 2020
@github-actions
Copy link

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 25, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants