azure-activedirectory-identitymodel-extensions-for-dotnet icon indicating copy to clipboard operation
azure-activedirectory-identitymodel-extensions-for-dotnet copied to clipboard

Add Async pattern for TokenValidation

Open MicahZoltu opened this issue 8 years ago • 69 comments

There are a number of places where JWTSecurityTokenHandler calls into user provided methods that may be doing some IO (e.g., database or remote calls) but they do so in a synchronous way.

One example of such a place is with JWT signature validation. The user can provide their own SignatureValidator or IssuerSigningKeyResolver. The act of validating a signature or resolving signing keys may require an interaction with an external server.

For a real-world example, Google rotates its signing keys regularly (rumor is daily) and you can get the latest public key in JWK format here. Unfortunately, it is not possible to know when they are going to roll their keys so an application that is attempting to validate Google issued JWTs will need to hit that endpoint to retrieve the latest signing key. The application could be optimized to cache the signing keys but that is still an async operation and it is entirely up to Google as to how often they rotate their keys (they could start rotating them for every request if they wanted).

Another real-world example once again with Google's authentication, Google supports sending the token to here for validation rather rather than doing the validation locally. If one wants to use this mechanism then every token validation would be a remote call.

Unfortunately, I recognize that the current API is very async un-friendly and the interfaces it implements are also not async-friendly but I wanted to get this filed anyway in hopes that at some point things can be improved to support modern asynchronous use cases.

MicahZoltu avatar Jul 05 '16 01:07 MicahZoltu

@Zoltu I agree with you that off-box validation is a real scenario. We should have an async pattern up and down the call graph to realize this pattern. We stopped at metadata retrieval.

brentschmaltz avatar Jul 05 '16 20:07 brentschmaltz

We will need to add an new return type: ValidationResult so the 'out' param is not needed.

brentschmaltz avatar Oct 12 '16 22:10 brentschmaltz

Include this issue with this fix. https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/566

brentschmaltz avatar May 05 '17 21:05 brentschmaltz

Modified title to reflect that this is general in nature across all token types. Perhaps in SecurityTokenHandler

brentschmaltz avatar Sep 14 '17 02:09 brentschmaltz

Do we have an ETA on this?

shagenhokie avatar Mar 20 '18 23:03 shagenhokie

@shagenhokie not yet

brentschmaltz avatar Mar 22 '18 18:03 brentschmaltz

@brentschmaltz the method that handles jwt authentication is async so I think it shouldn't be that difficult to add? https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerHandler.cs#L107-L128

It's a limitation if you want to implement token invalidation and call redis or any other db asynchronously inside the handler: https://stackoverflow.com/questions/52476325/how-to-invalidate-tokens-after-password-change/52476727#52476727

Because ValidateToken token uses out parameter and async methods can't be used with out parameter that would be a breaking change, ISecurityTokenValidator would need a method which returns Task anyway. The best workaround to avoid breaking change I can think of is:

public async Task<(ClaimsPrincipal, SecurityToken)> ValidateTokenAsync(string token,
    TokenValidationParameters validationParameters)
{
    var claimsPrincipal = base.ValidateToken(token, validationParameters, out var validatedToken); 
    return (claimsPrincipal, validatedToken);
}

public override ClaimsPrincipal ValidateToken(string token, TokenValidationParameters validationParameters,
    out SecurityToken validatedToken)
{
    var tuple = ValidateTokenAsync(token, validationParameters).Result;
    validatedToken = tuple.Item2;
    return tuple.Item1;
}

bugproof avatar Sep 25 '18 11:09 bugproof

@fragilethoughts I agree we need async, particularly with delegation to validation services, such as KeyVault and Google. Not only do we need ValidateTokenAsync, we need async on our Crypto such as SignatureProviders and KeyWrapProviders.

This is a feature we are thinking about for 5.3.1, but realistically it may be later. The other issue is we want to remove references to any libraries other than those shipped by Microsoft, such as JSON.net. With many JwtSecurityToken and JwtSecurityTokenHandler API's using JSON.net, we created a new assembly M.IM.JsonWebTokens (free from JSON.net) where ValidateToken returns a structure TokenValidationResult and can have easily have an async version. We are free to invent here.

brentschmaltz avatar Sep 26 '18 20:09 brentschmaltz

Sounds good

bugproof avatar Sep 26 '18 20:09 bugproof

Is the plan to create an async version of JwtSecurityTokenHandler, so it would be possible to call WriteToken asynchronously? It would help a lot, being able to go all async using IdentityServer4 for signing access tokens.

dhjf avatar Feb 26 '19 11:02 dhjf

Any status on this?

We have this issue as well where we need to do an http request to an HSM to sign access tokens. Our solution right now is to use WebClient.cs instead of the recommended way of doing http requests using HttpClient.cs. HttpClient does not support synchronous requests.

mcthuesen avatar Feb 27 '19 11:02 mcthuesen

@mcthuesen We haven't moved forward much on this, but it is important for a number of reasons. We are not sure if this fits into our 5.x release or will need to wait for 6.0. Our 6.0 release fits with CORE 3.0.

brentschmaltz avatar Mar 04 '19 21:03 brentschmaltz

Speaking on behalf of Azure DevOps, we'd love to have an async variant of the AudienceValidator delegate.

mfeingol avatar Apr 11 '19 00:04 mfeingol

Any idea when this will be available?

LeroyK avatar Oct 24 '19 14:10 LeroyK

@LeroyK at the earliest in the 1st qtr of 2020 for a full stack. However if Async Delegates were available would that satisfy a pent up need?

That may be simpler and we could stage the deliverable. Most likely, we would put the additions into JsonWebTokenHandler.

@mfeingol @mcthuesen @bugproof @shagenhokie @dhjf @MicahZoltu

Looking for feedback.

brentschmaltz avatar Oct 24 '19 16:10 brentschmaltz

Just came across this, was there any follow-up? I'm mostly interested in async versions of SignatureProvider.Sign and SignatureProvider.Verify

daniel-munch-cko avatar Feb 07 '20 14:02 daniel-munch-cko

@daniel-munch-cko we are working on shipping 6.x where we have a number of internal fixes being validated. Once that is out, we will address this as a lot of interest has been expressed. Particularly where crypto operations are performed out-of-process.

brentschmaltz avatar Feb 07 '20 18:02 brentschmaltz

This would be extremely helpful for our case where we have an ecosystem of services that communicate with each others APIs using JWT tokens, so they need to fetch signing keys on demand as they are rotated.

stebet avatar Feb 17 '20 11:02 stebet

@stebet that's exactly my usecase as well, it sucks having to do .Result on async method

cyberhck avatar Feb 17 '20 13:02 cyberhck

@stebet that's exactly my usecase as well, it sucks having to do .Result on async method

Shhhh... you'll invoke @davidfowl....

stebet avatar Feb 17 '20 13:02 stebet

@cyberhck @stebet @daniel-munch-cko yes we hear you. We have some internal work to do with our 6.x branch (our dev is ahead of dev5x). We do not want to backport this work, so the order of items is 6.x public, coordinating with asp.net, identityserver to ensure no breaking issues. Then we can address these long standing issues.

brentschmaltz avatar Feb 26 '20 22:02 brentschmaltz

Kinda funny to see it open since 2016.

bugproof avatar Feb 28 '20 09:02 bugproof

@bugproof fair enough. When opened the win was seen as a programming convenience, in a large number of cases, as almost all crypto operations were inproc and synchronous. That has changed as it is now common for part of the inbound validation for on out-of-proc or web request to be invoked.

brentschmaltz avatar Feb 28 '20 16:02 brentschmaltz

Hello @brentschmaltz , Just wants to be sure that there isn't any new update regarding the async versions of SignatureProvider.Sign and SignatureProvider.Verify?

Mordahlhuilhulh avatar Jul 05 '20 13:07 Mordahlhuilhulh

@Mordahlhuilhulh not yet ...

brentschmaltz avatar Jul 09 '20 16:07 brentschmaltz

@brentschmaltz Thanks for your quick replay. Do you know when the async version is planned?

Mordahlhuilhulh avatar Jul 12 '20 08:07 Mordahlhuilhulh

@Mordahlhuilhulh it looks like it's planned for V6 looking at assigned milestone.

cyberhck avatar Oct 28 '20 10:10 cyberhck

This might help anyone looking to do async JWT validation.

I ended up adding a new interface that also implements ISecurityTokenValidator. This allows me to add my custom validator to the JwtBearerOptions.TokenValidators collection and in my version of JwtBearerHandler (see #29917), I check to see if I'm working w/ the synchronous or asynchronous abstraction and act accordingly. The nice part about this approach is that it's a non-breaking change. The not so nice part is the potential for type checks, which I suppose could be hidden away behind an extension method, but still somewhat tacky.

public interface IAsyncSecurityTokenValidator : ISecurityTokenValidator
{
    /// <summary>
    /// Validates a token passed as a string using <see cref="TokenValidationParameters"/>
    /// </summary>
    ValueTask<(ClaimsPrincipal principal, SecurityToken validatedToken)> ValidateTokenAsync(
        string securityToken,
        TokenValidationParameters validationParameters
        );
}

public class CustomAsyncSecurityTokenValidator : JwtSecurityTokenHandler, IAsyncSecurityTokenValidator
{
    // optionally override the synchronous method
    // NOTE: You shouldn't let this actually be called, but it's better that this is called and the real validation happens
    //       rather than running the default `JwtSecurityTokenHandler.ValidateToken` code. You'll need to add custom
    //       IAuthenticationHandler to call the async method appropriately - see `JwtBearerAsyncValidationHandler` in #29917
    public override ClaimsPrincipal ValidateToken(
        string token,
        TokenValidationParameters validationParameters,
        out SecurityToken validatedToken
        )
    {
        ClaimsPrincipal principal;
        (principal, validatedToken) = ValidateTokenAsync(token, validationParameters).AsTask().GetAwaiter().GetResult();
        return principal;
    }

    public ValueTask<(ClaimsPrincipal principal, SecurityToken validatedToken)> ValidateTokenAsync(
        string securityToken,
        TokenValidationParameters validationParameters
        )
    {
        // do async stuff here
    }
}

mchandschuh avatar Feb 05 '21 18:02 mchandschuh

This might help anyone looking to do async JWT validation...

This is a workaround that does semi-work, but the sync-over-async implementation is risky as it might cause threadpool starvation on a high-traffic implementation.

We really need a proper async implementation here, but thanks for this workaround :)

stebet avatar Feb 08 '21 12:02 stebet

This might help anyone looking to do async JWT validation...

This is a workaround that does semi-work, but the sync-over-async implementation is risky as it might cause threadpool starvation on a high-traffic implementation.

We really need a proper async implementation here, but thanks for this workaround :)

If you look at the linked issue it also includes a custom authentication handler implementation that does a type check to see if the token validator is an IAsyncSecurityTokenValidator, so it's async all the way down. There shouldn't be any issues w/ this unless I'm misunderstanding something.

EDIT: Turns out I put the wrong code in the linked issue. I've updated it to properly invoke the IAsyncSecurityTokenHandler:

if (validator is IAsyncSecurityTokenValidator asyncValidator)
{
    (principal, validatedToken) = await asyncValidator.ValidateTokenAsync(token, validationParameters).ConfigureAwait(false);
    if (principal == null || validatedToken == null)
    {
        throw new SecurityTokenValidationException("The provided token failed validation.");
    }
}
else
{
    principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}

mchandschuh avatar Feb 23 '21 18:02 mchandschuh