Search code examples
c#async-awaitjwt

IssuerSigningKeyResolver call async method


We are using IssuerSigningKeyResolver which is part of Microsoft.IdentityModel.Tokens for our token validation and accepts non async delegate. We call a method which is async and that will result in a blocking call, so would like to know what will be the right way of using it.

IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
                {
                    return configurationManager.GetConfigurationAsync().ConfigureAwait(false).GetAwaiter().GetResult().SigningKeys;
                }

Solution

  • I had the same problem with our asp.net core (3.1) webapi,that uses AWS Cognito for authentication using an approach as documented here:

    https://medium.com/@marcio_30193/jwt-machine-to-machine-usando-aws-cognito-and-c-b1fab0524712

    This leads to a common block of code similar to @Punit like this;

    IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
    {
        // Get JsonWebKeySet from AWS
        var json = new WebClient().DownloadString($"{parameters.ValidIssuer}/.well-known/jwks.json");
        // Deserialize the result
        return JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
    }
    

    The problem with this code is that in asp.net core this will lead to thread pool starvation.

    The DownloadString method is Synchronous but interally it performs a .Result call causing the thread to block which can lead to thread pool starvation when you have a spike in requests.

    I found this blocking call using Ben.Blocking detector - very cool https://github.com/benaadams/Ben.BlockingDetector https://www.nuget.org/packages/Ben.BlockingDetector/

    So how to fix it?

    I tried, but I think it is impossible to resolve inside the delegate. The IssuerSigningKeyResolver is a Synchronous delegate, and cannot accept an async response. So even though the WebClient has DownloadStringAsyncTask it would make the response into a Task which IssuerSigningKeyResolver will not accept.

    The simple solution is to think outside the box (or delegate).

    With AWS Cognito, the data being requested is at a URL like the following;

    https://cognito-idp.{region}.amazonaws.com/{user-pool-id}/.well-known/jwks.json

    This returns a collection of JsonWebKeySet objects, which represents a set of encryption keys.

    I have confirmed with AWS that this data should NOT change for the pool as currently there is NO key rotation policy on the pool.. so..

    The solution is to request this data outside of the delegate method and just pass it in.

    This has two benefits;

    1. It fixes the problem
    2. It is more efficient.

    This delegate method is executed on EVERY request and therefore we are calling this .well-known/jwks URL for EVERY time and the response (per environment), NEVER changes!!

    The code for me looks like this;

        var issuingKeys = GetIssuerSigningKey(AuthSettings.CognitoUserPoolUrl);
    
            services.AddAuthentication(options => 
                ...
                IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
                {
                    return issuingKeys;
                }
                ...
            );
            
        private static IList<JsonWebKey> GetIssuerSigningKey(string cognitioUserPoolUrl)
        {
            var json = new WebClient().DownloadString($"{cognitioUserPoolUrl}/.well-known/jwks.json");
            return JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
        }
                
    

    In my case with AWS Cognito there is the risk that this data could change. My solution to this would be to start a background thread with a 30 second or 60 second timer and just refresh the static issueKey on that interval. If it ever did change, the site would have a small glitch for at most 30 seconds and then continue.

    In @Punit's example, he is getting the signingKeys from appSettings which 100% will NOT change, so the simple fix it to bypass the issue by obtaing your signingKeys outside of the delegate function in the setup code and just return it.

    The only other way to fix this would be if the IssuerSigningKeyResolver was converted to IssuerSigningKeyResolverAsync, possibly in a later version of .Net Core. (We are currently running our code in a Lambda, which only supports .net 3.1 at the moment so we have not attempted to update the version)

    Hope this helps.