Card image cap

Securing ASP.NET Core APIs with JWT Bearer using AWS Cognito

ASP.NET Core AWS Cognito  • Posted 5 months ago

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. 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
    }
}

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.

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).

Enjoying my posts?
You can now show me your support! 😊

What is the difference between Run() and Use() methods in IApplicationBuilder?

* Use() method: Used to create a simple middleware which can be "chained" to other functions over the pipeline. Takes two arguments: RequestDelegate ...


What is the difference between Response.Redirect() and Server.Transfer() ?

* Response.Redirect() redirects browser to another page, history is updated, trip back to client where browser loads the new page. * Server.Transfer( ...


How do you handle errors Globally in ASP.NET Core?

We can make use of the built-in UseExceptionHandler() middleware for catching Global Errors in ASP.NET Core. ``` app.UseExceptionHandler(err => ...


How do you design a strongly-typed class for a configuration?

To create a strongly-typed class for binding to a configuration section: * The property names and their types match the key names and their value t ...


How can you bind a configuration section to an object?

A Configuration section can be bound to a strictly-typed class object in two ways: * use Configuration.Bind() by passing the configuration section to ...


aws cognito asp.net core example aws cognito asp.net core api aws cognito and asp.net core amazon cognito asp.net core asp.net core cognito aws cognito asp.net core

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept