Welcome to this tutorial on how to implement and use JSON Web Tokens (JWTs) in your Express application to authorize users.
In this reading, you will learn how to manually parse, encode, and decode information using JavaScript's built-in string methods,
generate a secret token using Node's crypto library, and install and use the jsonwebtoken package to
sign, decode, and verify a JWT.
Think of a JWT as a special ticket that proves your identity when you're trying to enter a restricted area (like a concert or an exclusive event). If someone tampers with the ticket, it no longer matches the way it was originally stamped (signed), so it's easy to detect.
JWTs are commonly used in modern web applications to enable stateless authentication. Instead of storing login sessions on a server (like stamping your hand with invisible ink at an amusement park), JWTs give each user a token they carry around in their browser (like a metaphorical digital wristband). Whenever a user wants to do something that requires identity checks (like accessing a protected route), they present their token, and the server quickly checks if it's valid and hasn't expired.
This is especially useful in:
Imagine you purchase a plane ticket. The ticket includes your personal information (payload), some technical details in a header (like which airline is issuing it, date format, etc.), and an encoded stamp from the airline (the signature) that proves the ticket is real. At the airport, the airline scans your ticket, verifies that the stamp is valid, and then allows you to board. In JWT terms:
You might choose JWT in your application when:
However, JWTs come with certain caveats:
A JWT is just a string with three parts separated by periods (.):
header, payload, and signature. The format looks like this:
// JWT format
`${header}.${payload}.${signature}`
A real token might look like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG5ueUBnbWFpbC5jb20ifQ.SkuHIxgU1sDTrNKTTUIu9yDohUu8h0_4mbHiOMaUKwA
Below is a step-by-step breakdown of how you can work with a JWT using built-in JavaScript methods and
Node.js functionality.
Recall the JavaScript string methods you already know. You can use String.split() to split the JWT into its
three components at each period (.).
// Parsing a JWT
const sampleJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImpvaG5ueUBnbWFpbC5jb20ifQ.SkuHIxgU1sDTrNKTTUIu9yDohUu8h0_4mbHiOMaUKwA";
const jwtArray = sampleJwt.split("."); // returns an array
// [
// "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
// "eyJlbWFpbCI6ImpvaG5ueUBnbWFpbC5jb20ifQ",
// "SkuHIxgU1sDTrNKTTUIu9yDohUu8h0_4mbHiOMaUKwA"
// ]
const [ header, payload, signature ] = jwtArray;
console.log(header); // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
console.log(payload); // "eyJlbWFpbCI6ImpvaG5ueUBnbWFpbC5jb20ifQ"
console.log(signature); // "SkuHIxgU1sDTrNKTTUIu9yDohUu8h0_4mbHiOMaUKwA"
Once you have separated the header, payload, and signature, you can decode the header and payload using
Buffer.from() with 'base64' and .toString().
// Decoding a JWT's header and payload
const decodedHeader = Buffer.from(header, 'base64').toString();
console.log(decodedHeader); // {"alg":"HS256","typ":"JWT"}
const decodedPayload = Buffer.from(payload, 'base64').toString();
console.log(decodedPayload); // {"email":"johnny@gmail.com"}
However, if you try the same for the signature, you will see strange characters. This is because the signature is a hashed version of the header, payload, and the private key, which cannot be directly decoded into a human-readable format.
To re-encode a decoded header or payload, simply call Buffer.from() on the string,
and then apply .toString('base64').
// Encoding header and payload content for the JWT
const encodedHeader = Buffer.from(decodedHeader).toString('base64');
console.log(encodedHeader); // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
const encodedPayload = Buffer.from(decodedPayload).toString('base64');
console.log(encodedPayload); // eyJlbWFpbCI6ImpvaG5ueUBnbWFpbC5jb20ifQ==
This process shows that a JWT isn’t encrypted, it's just encoded. That means anyone who obtains your token can decode the header and payload easily. Never put sensitive data in the payload of a JWT.
Since the payload of a JWT can be easily decoded, you should not transmit protected information in the payload. The only part that is securely hashed is the signature. If someone tampers with the header or payload, the signature will no longer match, and the token will be considered invalid.
You can create the JWT’s signature by using crypto, the built-in Node.js cryptographic library.
Here is an example of creating the signature using your private key:
const signature = require('crypto')
.createHmac('sha256', privateKey)
.update(encodedHeader + '.' + encodedPayload)
.digest('base64');
To verify the JWT later, you would re-generate the signature using the same algorithm and private key, and check if it matches the token's signature. If it does, the token is valid. If not, the JWT has been tampered with or is invalid.
Manually splitting, encoding, decoding, and hashing works, but it can be tedious and error-prone. In real-world applications, it is best practice to rely on well-maintained Node packages that handle these steps for you.
Before you sign JWTs, you'll need a secret or private key. Think of this like a master stamp that only you have.
You’ll want it to be random and kept secret, so nobody else can forge valid tokens.
Use the crypto library in Node to generate a random secret:
require('crypto').randomBytes(64).toString('hex')
An example output might look like:
"dc1783e61ab05a9fa1b64d892f4b8edab51c159c7091d57feb955ad5ae8ce9191dbe3a50f95086a018654e6f3c7dbffd6215d656d63a2da811843fc746a664b2"
Store this in your .env file:
SECRET_KEY=dc1783e61ab05a9fa1b64d892f4b8edab51c159c7091d57feb955ad5ae8ce9191dbe3a50f95086a018654e6f3c7dbffd6215d656d63a2da811843fc746a664b2
Make sure you add .env to your .gitignore so it is never pushed to a public repository!
The jsonwebtoken package (often shortened to jwt) helps handle all of these processes for you.
Install it using:
npm install jsonwebtoken
Then import it into your file:
const jwt = require('jsonwebtoken');
From here, you can easily sign, decode, and verify JWTs in your application without manually juggling the string splitting or hashing.
To create a JWT, use the sign() method with a required payload and your secret key.
For instance, to create a token for “Johnny’s invite,” you might do:
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ email: "johnny@gmail.com" }, // payload object
process.env.SECRET_KEY, // secret token from .env file
{ expiresIn: '1h' } // options (e.g., expires in 1 hour)
);
This automatically builds a valid JWT string. You can customize the token using various options,
like algorithm, subject, audience, and more.
Check the jsonwebtoken documentation
for further details.
If you just need to read what’s inside the token (the payload), you can use decode():
const payload = jwt.decode(token);
// {"email":"johnny@gmail.com", "iat":...}
Note that decode() does not check if the token is valid or expired;
it only decodes the payload so you can read it.
The important step is verifying that a token is valid, hasn’t been tampered with, and hasn’t expired.
For that, use verify(), passing the token and the same secret used to create it:
const verifiedPayload = jwt.verify(token, process.env.SECRET_KEY);
// If the secret matches and the token is not expired,
// this returns the decoded payload; otherwise, an error is thrown.
If the token has been tampered with, the signature won’t match, so you’ll get a JsonWebTokenError.
If the token has expired based on your expiresIn setting, a TokenExpiredError will be thrown.
To solidify your understanding, follow these steps in a new or existing Node/Express project:
Preparation: Create a new Express app or open an existing one. Install dependencies:
npm install express dotenv jsonwebtoken.
Step: Generate a secret key:
require('crypto').randomBytes(64).toString('hex');
Copy this secret into your .env file under SECRET_KEY.
Step: In your Express app, create a route that signs a JWT:
const express = require('express');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const app = express();
app.get('/create-token', (req, res) => {
const token = jwt.sign({ email: 'user@example.com' }, process.env.SECRET_KEY, { expiresIn: '1h' });
res.send(token);
});
app.listen(3000, () => console.log('Server running on port 3000'));
Hit /create-token in your browser or with a tool like Postman. You’ll receive a JWT.
Copy this token somewhere safe.
Step: Create another route to verify the token:
app.get('/verify-token', (req, res) => {
const token = req.query.token; // For example, pass it as a query param
try {
const payload = jwt.verify(token, process.env.SECRET_KEY);
res.json({ valid: true, payload });
} catch (err) {
res.json({ valid: false, error: err.message });
}
});
Call /verify-token?token=YOUR_GENERATED_TOKEN in your browser/Postman.
You should see a message indicating whether the token is valid.
If it has expired or was tampered with, you’ll get an error response.
JWTs can be a powerful tool for managing user sessions in a stateless manner, especially in microservices and single page applications. However, always remember:
crypto module to generate a strong secret key and store it in a safe place (.env file).jsonwebtoken to sign, decode, and verify tokens instead of manually handling them.
Remember the difference between decode() and verify() in the
jsonwebtoken package:
In production, JWTs work best over secure channels (HTTPS), with short expiration times, and possibly with refresh tokens. Many large-scale applications and APIs rely on JWTs as a lightweight, portable token standard for authentication and authorization.
By following these steps and best practices, you’ll be able to confidently add JWT-based user authorization to your Node/Express applications. Think of JWTs like secure event tickets—hand them out, verify them at the door, and don’t let them sit around too long!