Mastering JWT in Node.js:
Secure Token Generation and Verification

Published on: August 25, 2024

JWT has gained popularity due to its simplicity, scalability, and stateless nature, making it a preferred choice for developers building modern web applications.

But what exactly is JWT?

JWT, or JSON Web Token, is an open standard (RFC 7519) used for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

Basic Components of a JWT Token
1{
2  "header": {
3    "alg": "HS256",  // The algorithm used to sign the JWT (e.g., HS256, RS256)
4    "typ": "JWT"     // The type of token, which is JWT in this case
5  },
6  "payload": {
7    "sub": "1234567890",  // The subject of the token, typically a unique identifier for the user
8    "name": "John Doe",   // An example claim that can include user-related data
9    "iat": 1516239022,    // The timestamp when the token was issued (Issued At)
10    "exp": 1516242622,    // The timestamp when the token will expire (Expiration Time)
11    "additional_claims": {
12      "role": "admin",             // Custom claim specifying the user's role
13      "permissions": ["read", "write", "delete"]  // Custom claim specifying the user's permissions
14    }
15  },
16  "signature": "sBv6QSPFZwAXCyIm8YH3FhTtMvwkP7tboz6CwRYg8B0"  // The encoded and signed signature for the JWT
17}
How does JWT ensure data integrity?

If any part of the JWT (header, payload, or signature) is tampered with after it has been signed, the signature will no longer match the data, making the token invalid. This integrity check ensures that the information within the JWT remains secure and unchanged during transmission.

You can verify this by visiting jwt.io

Generating and Verifying JWT Tokens in Node.js

jsonwebtoken is a library that allows you to create, sign, verify, and decode JSON Web Tokens (JWT) in Node.js applications.

Choosing an algorithm for signing JWT tokens

When choosing an algorithm for signing JWT tokens, it is essential to consider the following factors:

  • Security: Ensure that the algorithm is secure and not vulnerable to attacks.
  • Performance: Choose an algorithm that offers a good balance between security and performance.
  • Compatibility: Ensure that the algorithm is supported by the libraries and services you are using.

Refer to the Signing Algoritms section in the Auth0 documentation for more information.

We will be using the RS256 algorithm for signing our JWT tokens.

Why RS256?
  • Security: RS256 provides a strong level of security with public/private key pairs, making it suitable for applications where the token’s issuer and verifier might be different, such as in microservices architectures.
  • Scalability: Since the public key can be distributed openly, multiple services can verify tokens without needing access to the private key.
  • Industry Standard: RS256 is widely supported and used in many authentication and authorization systems, including OAuth 2.0 and OpenID Connect.
Generating RSA Key Pair

You can run the following command to generate an RSA private key on linux / unix

1ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key;

Removing newline characters in the private key

1awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}'  jwtRS256.key > jwtRS256.key;

Copy the contents of the private key and add it to your .env file.

Why set purpose in the JWT token?

Setting a purpose in the JWT token ensures that the token is used for the intended purpose only. This can help prevent misuse of the token and enhance security. In our case, we use purpose to differentiate between an access token and a refresh token.

Putting it all together
1import { default as jswt } from 'jsonwebtoken';
2
3// Default token payloads including issuer and signing algorithm
4const tokenPayloads: TokenPayloads = {
5	issuer: 'santhoshsiva.dev', // The entity that issues the token
6	algorithm: 'RS256', // The algorithm used to sign the token
7};
8
9// Define the purpose of the tokens
10const tokenPurpose = {
11	refresh: 'refresh', // Used for refresh tokens
12	access: 'access', // Used for access tokens
13};
14
15// Get the secret key from environment variables and escape newlines
16const { SECRET_KEY = '' } = process.env;
17const secretKeyEscaped = SECRET_KEY.replace(/\n/g, '
18'); // Ensure proper formatting of the secret key
19
20export const signToken = ({
21	uuid = '',
22	expiresInHours = '4h',
23	purpouse = '',
24	role,
25}: {
26	uuid: string;
27	expiresInHours?: string;
28	purpouse: string;
29	role: string;
30}) =>
31	new Promise<string>((resolve, reject) => {
32		jswt.sign(
33			{
34				uuid, // User ID
35				purpouse, // Token purpose
36				token_id: crypto.randomBytes(32).toString('hex'), // Unique token identifier
37				role, // User role
38			},
39			secretKeyEscaped, // The secret key used to sign the token
40			{
41				...tokenPayloads, // Include default payloads
42				audience: uuid, // Set the audience to the user's UUID
43				expiresIn: expiresInHours, // Set the expiration time
44			},
45			(err, token) => {
46				if (err) {
47					return reject(err); // Reject the promise if an error occurs
48				}
49				if (!token) {
50					return reject(new Error(errors.ERROR_SIGNING_TOKEN)); // Reject if the token is not generated
51				}
52				return resolve(token); // Resolve the promise with the generated token
53			}
54		);
55	});
56
57export const generateAccessToken = (uuid: string, role: string) =>
58	signToken({
59		uuid, // User ID
60		purpouse: tokenPurpose.access, // Set purpose to access
61		expiresInHours: process.env.IS_UNIT_TEST_MODE === 'true' ? '3s' : '1d', // Set expiration time based on environment
62		role, // User role
63	});
64
65export const generateRefreshToken = (uuid: string, role: string) =>
66	signToken({
67		uuid, // User ID
68		expiresInHours: '2d', // Set expiration time to 2 days
69		purpouse: tokenPurpose.refresh, // Set purpose to refresh
70		role, // User role
71	});
72
73interface TokenPayload {
74	uuid: string;        // Unique identifier for the user
75	iat: number;         // Issued At timestamp
76	exp: number;         // Expiration timestamp
77	purpouse: string;    // Purpose of the token (e.g., 'access', 'refresh')
78	role: string;        // User role (e.g., 'admin', 'user')
79}
80
81export const verifyToken = (token: string, reqPurpouse: string) =>
82	new Promise<TokenPayload>((resolve, reject) => {
83		// Verify the token using the secret key and token configurations
84		jswt.verify(
85			token,
86			secretKeyEscaped, // The secret key used to verify the token
87			{
88				algorithms: [tokenPayloads.algorithm], // Allowed algorithms for token signing
89				issuer: tokenPayloads.issuer,         // Expected issuer of the token
90			},
91			(err, decoded) => {
92				// Handle any errors that occur during verification
93				if (err) {
94					if (err.message === 'jwt expired') {
95						// Reject if the token has expired
96						return reject(new Error(errors.TOKEN_EXPIRED));
97					}
98					if (
99						err.message === 'invalid token' ||
100						err.message === 'jwt malformed'
101					) {
102						// Reject if the token is invalid or malformed
103						return reject(new Error(errors.TOKEN_INVALID));
104					}
105					// Reject for any other errors
106					return reject(err);
107				}
108
109				// If the token couldn't be decoded, reject the promise
110				if (!decoded) {
111					return reject(new Error(errors.TOKEN_DECODE_FAILED));
112				}
113
114				// Destructure the decoded token payload
115				const {
116					uuid = '',
117					iat = 0,
118					exp = 0,
119					purpouse,
120					role,
121				} = (decoded as TokenPayload) ?? {};
122
123				// Ensure that the required fields are present in the token
124				if (!uuid || !iat || !exp)
125					return reject(new Error(errors.TOKEN_DECODE_FAILED));
126
127				// Verify that the token's purpose matches the required purpose
128				if (purpouse !== reqPurpouse)
129					return reject(new Error(errors.TOKEN_INVALID));
130
131				// Resolve the promise with the decoded token payload
132				return resolve({ uuid, iat, exp, purpouse, role });
133			}
134		);
135	});
Conclusion

In this article, we learned about the basics of JWT, how to generate and verify JWT tokens in Node.js, and why RS256 is a popular choice for signing JWT tokens. We also discussed the importance of setting a purpose in the JWT token and how it can enhance security.

By following the best practices outlined in this article, you can ensure that your JWT tokens are secure and used for their intended purpose only.

What's Next?

Now that you have mastered JWT token generation and verification, you can explore more advanced topics such as token expiration, token revocation, and token renewal.

Check out the following blog posts to learn more about securing user sessions and implementing JWT authentication in your application:

Managing Sessions with JWT: Registering and Validating User Sessions

In this article