JWT Banner

From 0 to Hero - JSON Web Tokens (JWT)

JWT Standard (RFC 7519) — Unchanged Since 2015, Still Powering Simple and Secure Token-Based Authentication.

February 13, 2025

Token Definition

Tokens are pieces of data that carry information of a user's identity to allow a user to perform a certain action. Think of it as an ID to get into a restricted area, like a concert or a secure building, where only authorized people can enter.

Authentication vs Authorization

These are popular terms in modern computer systems that often confuse people. Both of these terms are related to security, some people even use them interchangeably, but they convey different meanings.

authentication vs authorization

Authentication

Authentication is the process of confirming the identity of a user or a device (entity). This is a means to confirm you are who you say you are. Think of proving to someone that a plane ticket is yours by showing your passport. Online, you typically prove your identity by providing something only you are supposed to know or have, such as an email and password combination or by clicking a link in an SMS received on your phone.

Authorization

Authorization refers to verifying what entities (users or devices) can access or perform an action. Think of it like being at a VIP event. The security guard doesn’t need to know who you are, whether you're the queen of England or a regular guest. What matters is whether you have a VIP ticket. A token can have both authentication and authorization purposes. Typically, once a user is signed in (authenticated), applications start caring about what they can do (authorization).

JWT Structure

JWT Structure

Header: Identifies which algorithm is used to encode the JWT.

Payload: Contains a set of claims (7 in total). Registered claims are standard fields commonly included in tokens. Custom claims are usually also included, commonly about a user. Private claims are specific to an application and agreed upon between parties to share additional information, such as user roles or permissions.

Signature: This is used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn’t changed along the way. The signature is created by encoding the header and payload with Base64 encoding, and a secret key, then signing them using the algorithm specified in the header.

This is how a JWT looks like:

eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRpb2dvIENhcmRvc28iLCJyb2xlIjoiZGV2IiwiaWF0IjoxNzM5MzY4MTk4LCJleHAiOjI1MjQ2MDgwMDB9.z-Xyf38WcCz9ibWD9Ye1yAmNLFOvvf2u7kv76gMW3RUomroo8kVTLm7US0tJzYt3f8vQiNoycySkR3kFvWNBqQ

Copy this token and go to jwt.io and paste it to see its contents.

JWT Payload Claims

None of this claims are mandatory but rather they provide a starting point. The 7 registered claims that come with JWT are the following:

Common JWT Signing Algorithms

The choice of algorithm depends on security requirements and the use case. The most common signing algorithms are the following.


HMAC (HS256, HS384, HS512): Symmetric signing using a shared secret key.

const secretKey = 'your-secret-key'; 
const payload = { userId: 123, role: 'user' };
const tokenHMAC = jwt.sign(payload, secretKey, { algorithm: 'HS256' });
const decodedHMAC = jwt.verify(tokenHMAC, secretKey);

A single shared secret key is used for both signing and verifying the JWT. This is the simplest method.


RSA (RS256, RS384, RS512): Asymmetric signing using a private/public key pair.

const privateKey = `-----BEGIN RSA PRIVATE KEY----- aBcD1234 etc.-----END RSA PRIVATE KEY-----`;
const publicKey = `-----BEGIN PUBLIC KEY----- eFgH5678 etc.-----END PUBLIC KEY-----`;
const payload = { userId: 123, role: 'user' };

const tokenRSA = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
const decodedRSA = jwt.verify(tokenRSA, publicKey);

SA uses a pair of keys: a private key to sign the JWT and a public key to verify the JWT. It is more secure for systems where you want to expose the public key for verification without compromising the signing key (private). Here is a website that generates RSA keys.


ECDSA(ES256, ES384, ES512): Asymmetric signing using elliptic curve cryptography.

const privateKey = `-----BEGIN EC PRIVATE KEY----- aBcD1234 etc.-----END EC PRIVATE KEY-----`;
const publicKey = `-----BEGIN PUBLIC KEY----- eFgH5678 etc.-----END PUBLIC KEY-----`;
const payload = { userId: 123, role: 'user' };

const tokenECDSA = jwt.sign(payload, privateKey, { algorithm: 'ES256' });
const decodedECDSA = jwt.verify(tokenECDSA, publicKey);

ECDSA is similar to RSA, but it uses elliptic curve cryptography, which is faster and produces smaller signatures. It is gaining popularity due to its efficiency.

Use Cases

This is what we all truly care! What are some common flows and use cases for JWTs?

Login Flow (Access Token Only)

Let’s start with the simplest case and then build up to a more robust approach.

login flow

When a user logs in, the server verifies their credentials and issues an access token. This token is sent in the response and used in subsequent requests, typically in the Authorization header, to access protected routes.

Once the token expires, the user is blocked from accessing endpoints and prompted to log in again to get a fresh access token.

This approach tho working is bad for user experience. Imagine having to sign in into your account every 15 minutes. You might be thinking “just make the access token last longer” but this introduces security risks.

If an attacker steals your token and it’s valid for let’s say, six months, they have access the entire time. Plus, storing long-lived tokens in headers increases exposure to interception or leaks, making them a risky choice.

So, how do we improve user experience while maintaining a high level of security? Welcome to refresh tokens!

Refresh Tokens and Silent Authentication

login flow with refresh token

By using a refresh token to issue new access tokens, the client (user using the app) does not need to manually re-authenticate. This process, called silent authentication or silent refresh, runs in the background, allowing seamless access without disrupting the user experience.

When the access token expires, the client automatically sends the refresh token to the server to obtain a new access token without requiring the user to log in again. The server verifies the refresh token, generates a new access token, and sends it back to the client. This new access token can either be returned in the response headers or stored in a new cookie.

When storing tokens in cookies, it's essential to use certain security flags to ensure they are protected:

While access tokens have a short lifespan (typically around 15 minutes), refresh tokens last longer and are used to obtain new access tokens. If both tokens expire, the user will need to log in again to generate new tokens.

Logout

Logout is straightforward: clear the access and refresh tokens stored in the cookies to terminate the user session. Since no tokens are available anymore, the user must be redirected to the login page to ensure they’re signed out and can’t access protected routes anymore.

Token invalidation should also take place, because even if the token removed from the cookie the token itself would still be valid. Mark the expiration date of the token as expired on the server side, ensuring that it is no longer usable.

Third-Party Authentication

When users authenticate with a third-party provider (e.g. Google, Facebook), they receive JWTs containing relevant profile information, such as name, picture, and email.

This topic deserves a deeper dive, as third-party authentication introduces its own complexities. However, it’s important to note that JWTs play a key role in this process, enabling secure and seamless integration with external authentication providers. JWTs are widely used across the globe in many contexts.

Security & Privacy

The contents of a JWT should always be cryptographically secured and never trusted blindly. The keys used to sign or encrypt the token must be carefully protected. It's crucial that these keys are private and under the control of the token issuer.

Even if a JWT is encrypted, it’s best to avoid including sensitive data. For example, don’t store passwords in the payload. Instead, include non-sensitive information like user ID or role.



Practice (Code Along)

Here are some popular libraries for working with JWTs in different programming languages:

This example will be with Node.js (JavaScript), but the principles are the same wherever you decide to follow along.

1. Prerequisites

Before you begin, ensure the following:

1. Have Node.js installed on your PC. You can install it here.

2. Have an IDE (I’m using VS Code).

2. Create Project

Open a folder (mine is called jwt) in VS Code and in the terminal type:

npm init -y

create project

Make sure you added "type": "module" and the script "dev" in package.json. Now install the dependencies in the terminal:

npm install express jsonwebtoken dotenv cookie-parser nodemon

create project

You should now have these dependencies in your package.json.

3. Create the app

Create a file called server.js and paste in the following code:

import express from "express";
import dotenv from "dotenv";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";

dotenv.config();
const app = express();
app.use(express.json());
app.use(cookieParser());

const PORT = 5000;

app.get("/", (req, res) => {
  res.send("Hello :)");
});

app.listen(PORT, () =>
  console.log(`Server running on port http://localhost:${PORT}`)
);

Open the terminal and type:

npm run dev

We have an app running. (you can ctrl click or cmd click on the link to open in the browser)

4. Safely store secrets

Create a .env file and paste in the following code:

PORT=5000
      ACCESS_TOKEN_SECRET=your_access_token_secret
      REFRESH_TOKEN_SECRET=your_refresh_token_secret

In server.js , change the hardcoded PORT to the one from the .env file:

.env file

The app is still running, but now the PORT number is safely stored in the environment file.

5. Generate tokens

Let’s first in server.js hardcode a user locally (in a real context the user would come from a database)

const users = [{ id: 1, username: "user1", password: "password", role: "admin" }];

Then let’s create a function that takes in a user to generate a JWT. And a function that takes in the generated token to decode it.

// Function to generate a JWT token
const generateAccessToken = (user) => {
  return jwt.sign(
    { id: user.id, username: user.username, role: user.role },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: "15m" }
  );
};
// Function to verify and decode a JWT token
const verifyToken = (token) => {
  try {
    return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
  } catch (err) {
    return null; // Return null if the token is invalid or expired
  }
}; 

We have implemented the simplest way of Signing Algorithms as we have talked about in HMAC Section We have used HMAC signing using a shared secret key (ACCESS_TOKEN_SECRET)

6. Creating routes

Let’s create some routes that use our newly created functions.

// Route to generate a token (Login)
app.post("/login", (req, res) => {
  const { username, password } = req.body;
  const user = users.find(
    (u) => u.username === username && u.password === password
  );

  if (!user) {
    return res.status(401).json({ message: "Invalid credentials" });
  }

  const accessToken = generateAccessToken(user);
  res.json({ accessToken });
});

// Route to verify a token
app.post("/verify", (req, res) => {
  const { token } = req.body;
  const decoded = verifyToken(token);

  if (!decoded) {
    return res.status(401).json({ message: "Invalid or expired token" });
  }

  res.json({ decoded });
});

This is how server.js looks like when finished.

final file

7. Testing!

Let’s test this! I advise you to use something like Postman or Insomnia.

postman 1

You have received a token by passing the credentials. Now a token with them was generated. Copy the accessToken and paste it in the following route.


postman 2

As you can see it worked! You have just encoded and decoded a user’s information using JWT!

The End

Link to this code: Github

Now the sky is limitless. There is much more to do. But this is it for now. The basic implementation of a JWT is done. Now you can try to create a new function for refresh tokens. Use cookies to store them and get them from there, and all the juicy stuff we have talked before.

If you want me to continue this tutorial to further depth please contact me on LinkedIn