How to secure APIs with JWT Tokens

Let's dig into securing our APIs by means of Authenticating incoming requests based on an access token with an example in ASP.NET Core

In the previous section we have looked at the concepts of Authentication and Authorization and how they differ from each other. We have also looked into various authentication schemes in place for implementing the same such as OpenID, OAuth and so on. Let’s dig into securing our APIs by means of Authenticating incoming requests based on an access token. Let’s also look at how we can write our own token generation provider using the asp.net core library.

Setting up the Context

Let’s assume we have a Readers API which exposes information about the Users and an API which exposes information about Readers present in the system. Now such an API when distributed can’t be accessed by everyone, and so we would need to look into ways to secure this API so that only authorized users can be allowed to fetch the data. In our scenario, we would like to Authenticate users and then issue them a key to access the resource.

To achieve this, we choose JWT Bearer authentication mechanism where on successful authentication of users we issue access tokens for a shorter period of time in a JWT (JSON Web Token) format. It can be understood as “give access to the bearer of the passed token”. In our case, we choose to issue our own tokens based on some validation criteria such as Login, while in real-world scenarios, we can make use of a third party Identity Providers such as Azure AD, AWS Cognito, Google, Facebook and so on.

The Validating Attributes

Basically, a JWT token is an encrypted JSON string with a payload which is signed using a standard algorithm such as RSA. A basic JWT token should consist of an Audience, Issuer, an Expiration Time, a SecretKey and Claims.

  • Audience: The recepient of this token or the receiver for whom the token was generated. When accessed an API via a token, we look at the audience attribute present in the token and validate against our set of valid audience so as to decide whether the owner of this token is allowed or not.
  • Issuer: The producer or the owner of the token who has issued this token for use. When accessed the API would look at the issuer present in the token and validates against the valid issuer from which the token was intended to be made.
  • Expiry: One of the most important features of tokens are their short life-spans. An access token must be as short as possible so that in case if a token is stolen, it would become unusable after a short period of time. The API would check if a passed token has already expired or still is alive.
  • SecretKey: While signing a JWT key before issuing, we encrypt the token using a secret key which is only known to the issuer token server. When a token is received in a request, we use the same signing key against the same encrypting algorithm to decrypt the token for its integrity. This makes the token sealed to any tampering.
  • Claims: This is an optional part, and we pass-in any required information about the user for whom the token represents. This can be helpful for any second level of validation, or for any business logic. In general, the access tokens avoid possessing sensitive user information, apart from non-application-specific information such as email, userid etc.

Validating JWT token is nothing but decrypting the token using the algorithm specified in the signature and examining the extracted JSON payload for a desired characteristic such as ExpiryTime, Issuer information, Audience and so on. Once a token passes all of the validating parameters above, it is said to be authorized and is good to access.

The Reader Example

Let’s look at the ReaderController code, which has two API endpoints, one which returns all the users and another endpoint we added which authenticates a user for his credentials and issues access token for use.

[Route("api/[controller]")]
[ApiController]
public class ReaderController : ControllerBase
{
    IReaderRepo _repo;
        
    public ReaderController(IReaderRepo repo)
    {
        _repo = repo;
    }

    [Authorize]
    [Route("all")]
    [HttpGet]
    public List<User> Get()
    {
        return _repo.Users;
    }

    [Route("token")]
    [HttpPost]
    public AuthResult Token([FromBody]LoginModel credentials)
    {
        return _repo.Authenticate(credentials);
    }
}

We have encapsulated the business logic for Reader in an abstraction IReaderRepo which is implemented by ReaderRepo as follows:

public interface IReaderRepo
{
    List<User> Users { get; }
    AuthResult Authenticate(LoginModel credentials);
}

public class ReaderRepo : IReaderRepo
{
    private static List<User> users;
    private ITokenManager _tokenManager;

    public ReaderRepo(ITokenManager tokenManager)
    {
        _tokenManager = tokenManager;
        SeedUsers();
    }

    private void SeedUsers()
    {
        users = new List<User>() {
            new User {
                Id = 1,
                Name = "Reader",
                Email = "reader1@me.com"
            }
        };
    }

    public List<User> Users => users;
        
    public AuthResult Authenticate(LoginModel credentials)
    {
        var user = users.FirstOrDefault(x => x.Email == credentials.Email);

        if (user != null)
        {
            return new AuthResult
            {
                IsSuccess = true,
                Token = _tokenManager.Generate(user)
            };
        }

        return new AuthResult { IsSuccess = false };
    }
}

For simplicity sake, we use a static user store (a list of hardcoded users) to authenticate incoming user credentials. And the Authenticate() method checks for matching user for the credentials passed and then issue a Token by calling the TokenManager.Generate() method within. The method also takes the fetched user object as parameter to pass suitable information of the user as claims.

The Token Manager Class

Its all simple till here, now we look at how we can create a jwt bearer token for use. We use the namespace System.IdentityModel.Tokens.Jwt for our purpose, which is a library of Jwt based token generation methods.

Note: Starting from ASP.NET Core 3.x, we are required to add the below NuGet packages to access the JwtBearerDefaults and Token generation libraries. We begin by adding the below packages to the project where the auth token is generated and where the Authentication middleware is created.

> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
> dotnet add package System.IdentityModel.Tokens.Jwt

The TokenManager is implemented as below:

public interface ITokenManager
{
    AuthToken Generate(User user);
}

public class TokenManager : ITokenManager
{
    public AuthToken Generate(User user)
    {
        List<Claim> claims = new List<Claim>() {
            new Claim (JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim (JwtRegisteredClaimNames.Email, user.Email),
            new Claim (JwtRegisteredClaimNames.Sub, user.Id.ToString())
        };

        JwtSecurityToken token = new TokenBuilder()
            .AddAudience(TokenConstants.Audience)
            .AddIssuer(TokenConstants.Issuer)
            .AddExpiry(TokenConstants.ExpiryInMinutes)
            .AddKey(TokenConstants.key)
            .AddClaims(claims)
            .Build();

        string accessToken = new JwtSecurityTokenHandler()
            .WriteToken(token);

        return new AuthToken() 
        {
            AccessToken = accessToken,
            ExpiresIn = TokenConstants.ExpiryInMinutes
        };
    }
}

The above code uses an object of type TokenBuilder which is then chained in series of methods which add Audience, Issuer, Expiry, Key and Claims to the token. Finally we generate the token by creating an instance of JwtSecurityTokenHandler class and invoking WriteToken() method with the created SecurityToken instance. The Tokens are handled by a class as shown below:

public class TokenConstants
{
    public static string Issuer = "thisismeyouknow";
    public static string Audience = "thisismeyouknow";
    public static int ExpiryInMinutes = 10;
    public static string key = "thiskeyisverylargetobreak";
}

In reality, we have these constants placed in the appsettings and read them by using the IConfiguration instance. (link to Typed class).

The TokenBuilder class used here is developed as shown below:

public class TokenBuilder
{
    private string _issuer;
    private string _audience;
    private DateTime _expires;
    private SigningCredentials _credentials;
    private SymmetricSecurityKey _key;
    private List<Claim> _claims;

    public TokenBuilder AddClaims(List<Claim> claims)
    {
        if (_claims == null)
            _claims = claims;
        else
            _claims.AddRange(claims);
        return this;
    }

    public TokenBuilder AddClaim(Claim claim)
    {
        if (_claims == null)
            _claims = new List<Claim>() { claim };
        else
            _claims.Add(claim);
        return this;
    }

    public TokenBuilder AddIssuer(string issuer)
    {
        _issuer = issuer;
        return this;
    }

    public TokenBuilder AddAudience(string audience)
    {
        _audience = audience;
        return this;
    }

    public TokenBuilder AddExpiry(int minutes)
    {
        _expires = DateTime.Now.AddMinutes(minutes);
        return this;
    }

    public TokenBuilder AddKey(string key)
    {
        _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
        _credentials = new SigningCredentials(_key, 
        SecurityAlgorithms.HmacSha256);
        return this;
    }

    public JwtSecurityToken Build()
    {
        return new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: _claims,
            expires: _expires,
            signingCredentials: _credentials
        );
    }
}

This is a custom Utility class which uses method chaining to create an instance of JwtSecurityToken returned when Build() method is called. When all of these components are grouped together we get an API which issues tokens on successful authentication of user credentials.

The Authentication Middleware

Now that we have obtained a token, we need to still have a mechanism to validate the passed bearer token inorder to decide whether to allow the request to pass through or be blocked from access. For that purpose we use the Authorization Middlewares provided for various types of authorization schemes in AspNetCore. We have JwtBearer(), OpenIdConnect(), Google(), Facebook(), Microsoft(), Twitter() and OAuth() among others for use. Ussing these we can implement user authentication and authorization. In our case, we go by JwtBearer() middleware since we are an API for which we authorize issued tokens.

Startup.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ITokenManager, TokenManager>();
        services.AddSingleton<IReaderRepo, ReaderRepo>();
            
	    // Custom extension method added that contains the actual logic
 	    services.AddBearerAuthentication();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
            app.UseMvc();
        }
    }

    // The Extension class for ServiceCollections
    static class AuthorizationExtension
    {
	    // Extension method for Adding 
        // JwtBearer Middleware to the Pipeline
        public static IServiceCollection AddBearerAuthentication(
            this IServiceCollection services)
        {
            var validationParams = new TokenValidationParameters()
            {
                ValidateAudience = true,
                ValidateIssuer = true,
                ValidateLifetime = true,
                IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(TokenConstants.key)),
                ValidIssuer = TokenConstants.Issuer,
                ValidAudience = TokenConstants.Audience
            };

            var events = new JwtBearerEvents()
            {
                // invoked when the token validation fails
                OnAuthenticationFailed = (context) =>
                {
                    Console.WriteLine(context.Exception);
                    return Task.CompletedTask;
                },
                    
                // invoked when a request is received
                OnMessageReceived = (context) =>
                {
                    return Task.CompletedTask;
                },

                // invoked when token is validated
                OnTokenValidated = (context) => 
                {
                    return Task.CompletedTask;
                }
            };
            
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme 
                    = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme 
                    = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = validationParams;
                options.Events = events;
            });

            return services;
        }
    }
}

In ConfigureService() method we add Authentication middleware to the pipeline and then attach JwtBearer() to specify what kind of authentication we’re using here. And we specify to use the Authentication scheme we defined in the pipeline by adding the UseAuthentication() in Configure method.

When the application boots up and the Startup class is parsed, the AspNetCore runtime registers the specified services in the container and then configures the Authentication Middleware pipeline and is invoked whenever a Controller API with an [Authorize] header decoration is invoked. This invokes the Authentication middleware and forces the request to be validated of its access token.

Inside the AddJwtBearer() method we specify what Audience, Issuer and Key we have used in generating token here. The Authentication middleware validates the input token using the exact same attributes. And when something differs, the middleware returns a 401 UnAuthorized response.

We have a provision to have any intermediatary logic when the Authentication middleware is invoked (OnMessageReceived), when the input token fails the validation (OnAuthenticationFailed) and when the token is successfully validated (OnTokenValidated). Once this is passed, the request goes to the Get() API and returns the data.

A Sample request for this API can be as follows:

POST /api/reader/token HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
	"email": "reader1@me.com"
}

and the Response –

{
    "isSuccess": true,
    "token": {
        "expiresIn": 10,
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxNGZhODkyMC00OGM3LTQ5NDctYjU0NC01Y2FlZTU4MzdhY2UiLCJlbWFpbCI6InJlYWRlcjFAbWUuY29tIiwic3ViIjoiMSIsImV4cCI6MTU3Mjk3Mjk4OSwiaXNzIjoidGhpc2lzbWV5b3Vrbm93IiwiYXVkIjoidGhpc2lzbWV5b3Vrbm93In0.SKQRYKG3sf5H8irXKI3xGg_pOKBexN1CNPXPT5winYQ"
    }
}

And using this Token to access the API:

GET /api/reader/all HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxNGZhODkyMC00OGM3LTQ5NDct
YjU0NC01Y2FlZTU4MzdhY2UiLCJlbWFpbCI6InJlYWRlcjFAbWUuY29tIiwic3ViIjoiMSIsImV4cCI6MTU3Mjk3Mjk
4OSwiaXNzIjoidGhpc2lzbWV5b3Vrbm93IiwiYXVkIjoidGhpc2lzbWV5b3Vrbm93In0.SKQRYKG3sf5H8irXKI3xG
g_pOKBexN1CNPXPT5winYQ

gives the Response:

[
    {
        "id": 1,
        "name": "Reader",
        "email": "reader1@me.com"
    }
]

In this way, we can secure our APIs using a simple JWT Bearer Authentication implementation.

Good Practices and Recommendations while using JWT Tokens –

One must keep in mind a few things while working with JWT tokens:

  • Keep the token lifetime as minimal as possible (less than 5 mins) so that even if the token is somehow extracted it can’t be replayed.
  • Ensure that the claims of the token doesn’t not pass any sensitive information of the user.
  • Ensure that the API which issues and accepts tokens must run on HTTPS only with strict restrictions on Domain (CORS policies) access.
  • The API can also store and validate a field called jti (an arbitary value generated by the API to uniquely identify a state) which it can use to detect token replay attacks and immediately invalidate all the tokens issued to the user and log him out of the application.
  • When using in an SPA architecture or for storing JWT tokens on client side using LocalStorage might be a simpler option for developers and application access, but it has its own set of security risks. Any application can read LocalStorage and access the tokens which is a very serious risk.
  • Consider tweaking the architecture such that the client makes use of a special kind of cookie called HttpOnly cookie which is set by the server onto the client, which can’t be read by any application and is automatically passed onto the server for each request. That would be one of the most secure options for storing and passing tokens for most scenarios.

The sample application used in the above illustration is available at: https://github.com/referbruv/role-policy-authorization-sample

Buy Me A Coffee

Found this article helpful? Please consider supporting!


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.

2 Comments

    • Hello there. JTI stands for JWT ID, which is expected to be a unique identifier generated for every JWT. In an ideal scenario, any two JWT tokens issued must not have the same JTI and the API can store and verify if the next token also contains the same JTI and reject if true. But this will only increase storage on the API side because now it has to store all the incoming JTI. Generally this JTI is created by the Token Server when creating a new Token. Now what is this Token Server? Any Authorization Server that issues Tokens, you can checkout IdentityServer4 or AWS Cognito which I had explained in detail.

      Hope that helps!

Leave a Reply

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