Implement JSON Web Token (JWT) Authentication using AccessToken and RefreshToken

You must have heard the proverb that “One leak will sink a ship”. So, in this blog, we will learn to design how to make our backend services non-sinkable. I will also share my experiences with creating secure web APIs over the years and point out the critical things that must be taken care of in the security design.

The Need for Authentication

The first and the most basic requirement of the web APIs is to prevent unauthorized access to the services. For example, how would we feel if anyone can read or send personal emails from our account? We would obviously never visit that website or app again. So, what can we do to solve this problem?

The simplest way to solve this problem is also the oldest one. We add a registration process, in which a user will register using a unique identifier like email and choose a password. For all the subsequent requests the user will send that unique identifier and his password (most preferably in HTTP headers). The server will validate the registration of that user and only when the password matches he/she will be allowed to read/send the emails.

This is called the Basic Authentication scheme and is defined in RFC 7617. In this scheme, we send the unique identifier and password pair encoded using base64. For example, we combine the credentials in colon-separated string i.e."username:password" and then encode in base64. We should send this string in the HTTP header, Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=. Since, we are sending a text over the network, which can be decoded, we should always use Basic scheme along with HTTPS/TLS.

But, in the Basic scheme, we will have to send the credentials over the wire with each request but we would not want to store the credentials locally. So, we can implement: Signup/Login → Call Other APIs

How can we implement it? Let's think...

Token Solution

When a user registers then the backend server generates a token and sends it to the client. That token is sent back to the backend for each subsequent API call by the client through the HTTP Authorization header. This token has a small-time validity and can not be used after its expiration time. To receive a new token the user logs in again with his credentials [we should always use the HTTPS/TLS connection to send the user credentials].

Now, we would want to implement this token authentication system. So, we will have to understand how to define the structure of the token to ensure its usability and security.

Token Content

Through a consensus, a standard for the structure of the token is adopted and documented in the RFC 7519. This token is called JSON Web Token (JWT).

Let's first take an example of such a token from our open source project Node.js Backend Architecture Typescript Project.

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZnRlcmFjYWRlbXkuY29tIiwiYXVkIjoiYWZ0ZXJhY2FkZW15X3VzZXJzIiwic3ViIjoiNWU3Yjk1OTIzMDg1ODcyZDNjMzc4ZjM1IiwiaWF0IjoxNTg1MTU4ODc4LCJleHAiOjE1ODc3NTA4NzgsInBybSI6IjNlZDcyM2NiYjI3YmVmNjVkNmUwMzI1NTBlNDNmOTBhZjczNmFjNDJjOTUxYzBmNjljMzE2ZTU4NTRlZjc4NDczYjNkMzc0ZjI4ZWNiZTFkN2FmYTNlYjQ5YjVjYjM2MjY5ZWI0N2YyMjAxOTc0ODRiZGQyNDc4NzE4NzkxODFhIn0.OotEsfdYe3DYTM6fMvsiMmG9TBgJyL4MOpi-cDi2wB1Qzgq__klHLZzB1cEYat0l8vPXm4Zok_yX4VvRn4cWDw

This is a JWT. The main objective of having this structure is listed below:

  • We can add data in the token to identify the context.
  • We can sign the token to ensure its authenticity.
  • We can add the metadata for token processing instructions.

This JWT is a base64 encoded JSON string having 3 parts separated by a (.) dot.

Note: You can convert base64 string using atob('base64string') function in JavaScript or you can paste your token here: https://jwt.io to get its contents.

Let's see these individual parts in the token from the above example.

// Part 1
atob('eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9')
Output: "{"alg":"RS256","typ":"JWT"}"

// Part 2
atob('eyJpc3MiOiJhZnRlcmFjYWRlbXkuY29tIiwiYXVkIjoiYWZ0ZXJhY2FkZW15X3VzZXJzIiwic3ViIjoiNWU3Yjk1OTIzMDg1ODcyZDNjMzc4ZjM1IiwiaWF0IjoxNTg1MTU4ODc4LCJleHAiOjE1ODc3NTA4NzgsInBybSI6IjNlZDcyM2NiYjI3YmVmNjVkNmUwMzI1NTBlNDNmOTBhZjczNmFjNDJjOTUxYzBmNjljMzE2ZTU4NTRlZjc4NDczYjNkMzc0ZjI4ZWNiZTFkN2FmYTNlYjQ5YjVjYjM2MjY5ZWI0N2YyMjAxOTc0ODRiZGQyNDc4NzE4NzkxODFhIn0')
Output: "{"iss":"afteracademy.com","aud":"afteracademy_users","sub":"5e7b95923085872d3c378f35","iat":1585158878,"exp":1587750878,"prm":"3ed723cbb27bef65d6e032550e43f90af736ac42c951c0f69c316e5854ef78473b3d374f28ecbe1d7afa3eb49b5cb36269eb47f220197484bdd247871879181a"}"

// Part 3
atob('OotEsfdYe3DYTM6fMvsiMmG9TBgJyL4MOpi-cDi2wB1Qzgq__klHLZzB1cEYat0l8vPXm4Zok_yX4VvRn4cWDw')
Output: ???

Let's convert these JSON strings in the JSON objects:

// Part 1
{
  "alg": "RS256",
  "typ": "JWT"
}

// Part 2
{
  "iss": "afteracademy.com",
  "aud": "afteracademy_users",
  "sub": "5e7b95923085872d3c378f35",
  "iat": 1585158878,
  "exp": 1587750878,
  "prm": "3ed723cbb27bef65d6e032550e43f90af736ac42c951c0f69c316e5854ef78473b3d374f28ecbe1d7afa3eb49b5cb36269eb47f220197484bdd247871879181a"
}

// Part 3
???

The Three Parts of the JsonWebToken:

  1. Header: It contains the metadata for the signing method and the encryption method applied to the token.
  2. Payload: It contains information about the authenticated user. The RFC defines and recommends some standard keys for this JSON object but it is not mandatory and we can add any data in this payload.
  3. Signature: This part is very important. We can ask this question that if all the information is in base64 and anyone can decode this data. So, it does not prevent from creating a fake token on behalf of someone else with this information and access his personal data. Then how can it be a secure way of authentication?

So, the signature is what makes this whole system work. We encrypt the hash of the contents of the token, i.e. Header and Payload using the encryption algorithms like RS256 (RSA Signature with SHA-256), HS256 (HMAC with SHA-256), etc. The third part of the token contains this encrypted hash string.

In the project, I have used the RS256 approach. I find it more useful as the client can verify the authenticity of the token using the public key.

In RS256 we have a private key and a public key pair (you can find these keys in the project at /keys directory). The data hash can be encrypted using the private key and can only be decrypted using the public key. This private key needs to be kept secure on the server filesystem. The public key can be shared with the client if required.

To validate the authenticity of the token, the Header, and the Payload (base64 string parts of the token) are combined and hashed together and the hash is then validated using the public key to test whether it was actually encrypted using its private key. This ensures that no one else can either change the original data in the token or send false data. Because the hash value will not match the originally issued token.

Now, we are assured that no one can hamper with the token, let's understand the Payload data. We can add any JSON data in the payload but these are some standard keys that should be added for the universality of the structure.

  1. iss (Issuer): This identifies the issuing authority of the token. In most of the cases, it is the name of the authentication server. In our project, we have used "afteracademy.com".
  2. aud (Audience): This identifies the recipients of the token who will process the JWT and use the information stored in the token. In our project, it is our application server, "afteracademy.com"
  3. sub (Subject): It identifies the end-user of the token. It usually contains the database user id.
  4. iat (Issued At): It defines the time when the token was issued.
  5. exp (Expiration Time): It defines the time after which the token is considered expired and is deemed invalid.
  6. prm (Param): It is a custom field that I have added to identify the token for the user so that we can force invalidate that token when needed. It is explained further in this blog.

AccessToken

Signup/Login → Receive Token → Send that Token in other APIs

In our open-source project API Doc, you can find the complete structure of the APIs. Let's take Login example:

POST /v1/login/basic HTTP/1.1
Host: localhost:3000

Request Header:
x-api-key: ASSIGNED_API_KEY
Content-Type: application/json

Request Body:
{
   "email": "ali@mindorks.com",
   "password": "changeit"
}

Response Body:
{
  "statusCode": "10000",
  "message": "Login Success",
  "data": {
    "user": {
      "_id": "5e7b95923085872d3c378f35",
      // other user data
    },
    "tokens": {
      "accessToken": "NEW_JWT_FOR_ACCESS",
      "refreshToken": "NEW_JWT_FOR_REFRESH"
    }
  }
}

Now you can use the received data.tokens.accessToken in the other APIs. [We will understand refreshToken use case further in this blog]. The AccessToken and RefreshToken are stored securely on the client-side, so that the user does not have to re-login each time he/she opens the website or app.

It is accepted in the backend community that this JWT should be sent in the Authorization header with Bearer scheme. Although this scheme is defined for OAuth2 authentication, it is also recommended in JWT case.
POST /v1/writer/blog HTTP/1.1
Host: localhost:3000

Request Header:
x-api-key: ASSIGNED_API_KEY
Content-Type: application/json
Authorization: Bearer RECEIVED_JWT_FOR_ACCESS

Request Body:
{
   "title": "SOME_TITLE",
   // other blog data
}

Response Body:
{
  "statusCode": "10000",
  "message": "Blog created successfully",
  "data": {
     // created blog data
  }
}

RefreshToken

You may realize that the AccessToken received by the client is being used for a specific period let's say 6 hours from the time of the issue. During this period the user can use the website or app without login again. But, potentially it will be a threat for misuse by an attacker.

We can minimize this vulnerability by issuing a token for a smaller duration but then the user-friendliness decreases. So, we can agree on a reasonable period for token validity but the question now arises that what happens after that period, i.e. when the token expires?

There are two ways to handle this case as described below:

  1. Forced Logout: Users will have to re-login on the website/app but this will lead to bad user experience.
  2. RefreshToken: The main vulnerability of the credential is increased when it is shared frequently. To receive a token we need credentials and we can not store those credentials on the client-side for automatic re-login as it is a security hazard because it does not have an expiration time. But, we can extend a token's validity period by allowing to re-issue a new token when it expires.

When we login/signup we receive 2 tokens, AccessToken having validity of less time (example, 24 hours) and a RefreshToken which has a higher validity time (example, 3 months). After the AccessToken is expired we can send its RefreshToken to receive a new AccessToken and a corresponding new RefreshToken. The RefreshToken can only be used once and then it becomes invalid.

Let's see the refresh token API from our project.

POST /v1/token/refresh HTTP/1.1
Host: localhost:3000

Request Header:
x-api-key: SUPPLIED_API_KEY
Authorization: Bearer RECEIVED_JWT_FOR_ACCESS
Content-Type: application/json

Request Body:
{
   "refreshToken" : "RECEIVED_JWT_FOR_REFRESH"
}

Response Body:
{
  "statusCode": "10000",
  "message": "Token Issued",
  "accessToken": "NEW_JWT_FOR_ACCESS",
  "refreshToken": "NEW_JWT_FOR_REFRESH"
}

Token Storage on Frontend

You can analyze that the token is not secure if a user shares his/her device with some other person or someone tries to access the storage where you store this token through some other means like cross-site scripts (XSS). So, it is recommended not to store these tokens in places that can be accessed by the JavaScript code. Cookies are also vulnerable to cross-site request forgery (CSRF) attacks.

So, what is the best possible place to store the tokens securely? You can read more about it on the internet if you are passionate to achieve completely secure storage. Some of the solutions are ideal but not very practical. Practically I would store it in the Cookies with httpOnly and Secure flags. It is not 100 percent secure but it gets the job done.

Implementing Authentication using JWT

You can find the complete implementation of authentication using tokens in our open-source project: Node.js Backend Architecture Typescript Project. We have used the jsonwebtoken library in Node.js for creating and validating JWTs. In the project, you should explore these files.

  • /src/core/JWT.ts: This file has utility functions for encoding, decoding, and validating tokens.
  • /src/auth/authentication.ts: This file has express middleware to check the authentication logic for an API using JWTs.
  • /src/routes/access/signup.ts: This file handles the signup request, creates a user and issues tokens.
  • /src/routes/access/login.ts: It validates the user credentials and issues tokens.

Token Handling Edge Cases

Few things have been missed in the above approach.

  1. How can we force invalidate a token in cases it is required? For example, if the user data is compromised and we have to invalidate the tokens issued for all the users. We can certainly write this logic in backend code but it will be an ugly way to implement such specific if-else checks based on the "iat".
  2. How to give this feature to the user to log-out from all existing devices. In this case, we need to invalidate one particular user's all issued token. The backend application check based on iat will not work in this case.
  3. How to restrict RefreshToken to be used only for re-issue of the tokens and not as an alternative to AccessToken?
One way to handle these cases are to store the JWT in the database. But to prevent the misuse we can store the MD5 hash (with salt) of the JWT, similar to the password storage in the project. We will also have to map the AccessToken to the corresponding RefreshToken. I prefer to do this in a different way. So, what I have done in the project is to generate a random hash and embed it in the token's "prm" and store that hash in the database. You can also use a unique id generator for each token as "prm" but random hash also works for me.

In our project, we have created a model "Keystore" specifically for these cases. Let's explore this Model at /src/database/model/Keystore.ts

export default interface Keystore extends Document {
   client: User;
   primaryKey: string;
   secondaryKey: string;
   status?: boolean;
   createdAt?: Date;
   updatedAt?: Date;
}
  • client: Its the user for which the AccessToken and RefreshToken has been issued.
  • primaryKey: This is the "prm" field in the Payload of the JWT [AccessToken] that we saw earlier.
  • secondaryKey: This is the "prm" field in the Payload of the JWT [RefreshToken].
  • status: This can be used to force invalidate the AccessToken and RefreshToken pairs by setting it to false.

Token Validation Logic

  1. Extract the Bearer Authorization header and extract the JWT.
  2. Decode the JWT payload and get the sub (user id).
  3. Check the database for the existence of that user.
  4. Verify the JWT signature and the payload.
  5. Check the Keystore for the existence of JWT for the given user.
  6. If the condition fails at any step then send 401.
  7. If all the conditions are okay then forward the request to its corresponding route handler.

Conclusion

I am happy that you came this far. I hope that you will now be able to appreciate the intricacies of authentication using JsonWebToken and implement the same with confidence in your project.

GitHub Repository Link: Node.js Backend Architecture Typescript Project

Let me know your feedback in the comments below and also mention other topics you would want me to write.

Please, share this blog so that others can learn from it.

Take Care and Keep Learning