Securing ASP.NET Core API with JWT Token using AWS Cognito

In this article, let's look at how we can validate a AWS Cognito User JWT token in an ASP.NET Core API in a step by step guide

Introduction – Recap

In a previous article, we have discussed in detail about what AWS Cognito is and how it helps applications delegate their Authentication module to AWS Cloud and let AWS do the heavy lifting for them, providing a secure and scalable solution for modern day application needs.

We have also looked at the UserPools and how to create a UserPool with an AppClient which takes care of the User Management and provides validation via Tokens.

In this article, let’s look at how this UserPool created is used in an actual application perspective – how we can secure an application using UserPool created in AWS Cognito. We shall use an ASP.NET Core API as an example for our demonstration and link the Cognito application to it.

JWT Auth in ASP.NET Core with Cognito

The expectation is that when a user authenticated in AWS Cognito and obtained a Token tries to access the API using the Token, the API must be able to validate the Token for its authenticity and let the user pass or deny access.

It is very simple in case of ASP.NET Core, since it comes with a customizable Authentication middleware which we feed appropriate validation parameters and the middleware takes care of validating the Tokens issued by Cognito and sets up the Claims if successful.

To get started, we need to take note of a few values from AWS Cognito UserPool that we have created previously. The required ones are:

UserPoolId

  • which uniquely identifies a AWS Cognito UserPool and which manages the Users. You can find it at the top of the UserPool page under the Pool name.

AppClientId

  • which grants an application to connect to and access AWS Cognito for reading and writing User data. You can find it in the AppClient Settings page below the Client Name.

Region

  • where the Cognito is currently created. You can find it on the top right of the Console. It’ll be a place name where the AWS data center operates. (N. Virginia, Oregon etc). Click on the name which reveals its region name (For example, Oregon is us-west-2 and so the Region value is us-west-2).

With these values taken, let’s begin in our AspNetCore API by adding the Authentication.JwtBearer package since it needs to be added separately since AspNetCore3.1

> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Once added, let’s add the Authentication middleware in the Startup class and configure the TokenValidationParameters that the middleware picks up to validate an authentic Token against a malicious one.

services.AddAuthentication()
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = GetCognitoTokenValidationParams();
    });

The method GetCognitoTokenValidationParams() is a private method we shall write which returns an instance of TokenValidationParameters(). The middleware uses this instance during runtime and configures itself to act according to what is configured inside the instance.

private TokenValidationParameters GetCognitoTokenValidationParams()
{
    var cognitoIssuer = $"https://cognito-idp.{Configuration["region"]}.amazonaws.com/{Configuration["userPoolId"]}"; 
    
    var jwtKeySetUrl = $"{cognitoIssuer}/.well-known/jwks.json";

    var cognitoAudience = Configuration["appClientId"];

    return new TokenValidationParameters
    {
        IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
        { 
            // get JsonWebKeySet from AWS 
            var json = new WebClient().DownloadString(jwtKeySetUrl);
            
            // serialize the result 
            var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
            
            // cast the result to be the type expected by IssuerSigningKeyResolver 
            return (IEnumerable<SecurityKey>)keys;
        },
        ValidIssuer = cognitoIssuer,
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidAudience = cognitoAudience
    };
}

Observe that we’re downloading the KeySet from an endpoint inside the UserPool. This is because the AWS Cognito rotates its keys frequently so that the JWT tokens can’t be forged easily.

Finally, we add this middleware to operate in the Request Pipeline by tagging in the Configure() method as below:

app.UseAuthentication();

We’re done with the Authentication middleware setup of AWS Cognito within our ASP.NET Core application. To test this, we can take up a token produced by logging a user in the default Hosted Login UI provided with Cognito. Once the token is fetched, we shall pass it to any endpoint which is decorated by [Authorize] attribute.

[ApiController]
[Route("api/[controller]")]
public class ReaderController : ControllerBase
{
    [Authorize]
    [Route("{id}")]
    public IEnumerable<Reader> Read(int id)
    {
        // Authenticated users only can process this
    }
}

How Cognito Token is Validated?

As soon as a request comes up to this endpoint, the Authentication middleware is triggered due to the Authorize decoration and the Token passed in the Authorization header in the request is examined for the request. The middleware parses the Token and checks for these:

  1. The Issuer – If the iss attribute matches with the cognitoIssuer we’ve configured which is the URI containing the userPoolId.
  2. The Audience – If the aud attribute matches with the appClientId we’ve configured as the ValidAudience in the middleware.
  3. The Signature – If the Signature of the Token matches with the signature fetched from the jwtKeySet downloaded from the UserPool.
  4. Expiry – If the token lifetime has not expired by the time it is being examined in the middleware (an obvious check).

Once the above conditions are met, the middleware marks the AuthenticationContext as Succeeded and attaches the Claims read from the Token to the User property of the HttpContext. The request is then allowed to access the API and we can also access the claims which are available under the Context.User.Claims Dictionary.

Conclusion

In this way, we can secure an ASP.NET Core API by means of JWT Bearer Authentication using AWS Cognito UserPools. By the way, what was the token we’re using in this context? it is called as CUP Token (Cognito User Pool Token).


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

3 Comments

  1. Hi Sriram, thank you for the article. In is interesting for me.
    I am developing a Lambda (NET6) service. A Client app will develop other developers.
    Could you write yet how to test via Swagger but using Cognito authentication/authorization?
    Thank you.
    Regards,
    Oleg.

Leave a Reply

Your email address will not be published. Required fields are marked *