JWT has gained popularity due to its simplicity, scalability, and stateless nature, making it a preferred choice for developers building modern web applications.
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.
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}
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
jsonwebtoken is a library that allows you to create, sign, verify, and decode JSON Web Tokens (JWT) in Node.js applications.
When choosing an algorithm for signing JWT tokens, it is essential to consider the following factors:
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.
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.
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.
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 });
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.
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