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.
{
"header": {
"alg": "HS256", // The algorithm used to sign the JWT (e.g., HS256, RS256)
"typ": "JWT" // The type of token, which is JWT in this case
},
"payload": {
"sub": "1234567890", // The subject of the token, typically a unique identifier for the user
"name": "John Doe", // An example claim that can include user-related data
"iat": 1516239022, // The timestamp when the token was issued (Issued At)
"exp": 1516242622, // The timestamp when the token will expire (Expiration Time)
"additional_claims": {
"role": "admin", // Custom claim specifying the user's role
"permissions": ["read", "write", "delete"] // Custom claim specifying the user's permissions
}
},
"signature": "sBv6QSPFZwAXCyIm8YH3FhTtMvwkP7tboz6CwRYg8B0" // The encoded and signed signature for the JWT
}
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
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key;
Removing newline characters in the private key
awk '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.
import { default as jswt } from 'jsonwebtoken';
// Default token payloads including issuer and signing algorithm
const tokenPayloads: TokenPayloads = {
issuer: 'santhoshsiva.dev', // The entity that issues the token
algorithm: 'RS256', // The algorithm used to sign the token
};
// Define the purpose of the tokens
const tokenPurpose = {
refresh: 'refresh', // Used for refresh tokens
access: 'access', // Used for access tokens
};
// Get the secret key from environment variables and escape newlines
const { SECRET_KEY = '' } = process.env;
const secretKeyEscaped = SECRET_KEY.replace(/\n/g, '
'); // Ensure proper formatting of the secret key
export const signToken = ({
uuid = '',
expiresInHours = '4h',
purpouse = '',
role,
}: {
uuid: string;
expiresInHours?: string;
purpouse: string;
role: string;
}) =>
new Promise<string>((resolve, reject) => {
jswt.sign(
{
uuid, // User ID
purpouse, // Token purpose
token_id: crypto.randomBytes(32).toString('hex'), // Unique token identifier
role, // User role
},
secretKeyEscaped, // The secret key used to sign the token
{
...tokenPayloads, // Include default payloads
audience: uuid, // Set the audience to the user's UUID
expiresIn: expiresInHours, // Set the expiration time
},
(err, token) => {
if (err) {
return reject(err); // Reject the promise if an error occurs
}
if (!token) {
return reject(new Error(errors.ERROR_SIGNING_TOKEN)); // Reject if the token is not generated
}
return resolve(token); // Resolve the promise with the generated token
}
);
});
export const generateAccessToken = (uuid: string, role: string) =>
signToken({
uuid, // User ID
purpouse: tokenPurpose.access, // Set purpose to access
expiresInHours: process.env.IS_UNIT_TEST_MODE === 'true' ? '3s' : '1d', // Set expiration time based on environment
role, // User role
});
export const generateRefreshToken = (uuid: string, role: string) =>
signToken({
uuid, // User ID
expiresInHours: '2d', // Set expiration time to 2 days
purpouse: tokenPurpose.refresh, // Set purpose to refresh
role, // User role
});
interface TokenPayload {
uuid: string; // Unique identifier for the user
iat: number; // Issued At timestamp
exp: number; // Expiration timestamp
purpouse: string; // Purpose of the token (e.g., 'access', 'refresh')
role: string; // User role (e.g., 'admin', 'user')
}
export const verifyToken = (token: string, reqPurpouse: string) =>
new Promise<TokenPayload>((resolve, reject) => {
// Verify the token using the secret key and token configurations
jswt.verify(
token,
secretKeyEscaped, // The secret key used to verify the token
{
algorithms: [tokenPayloads.algorithm], // Allowed algorithms for token signing
issuer: tokenPayloads.issuer, // Expected issuer of the token
},
(err, decoded) => {
// Handle any errors that occur during verification
if (err) {
if (err.message === 'jwt expired') {
// Reject if the token has expired
return reject(new Error(errors.TOKEN_EXPIRED));
}
if (
err.message === 'invalid token' ||
err.message === 'jwt malformed'
) {
// Reject if the token is invalid or malformed
return reject(new Error(errors.TOKEN_INVALID));
}
// Reject for any other errors
return reject(err);
}
// If the token couldn't be decoded, reject the promise
if (!decoded) {
return reject(new Error(errors.TOKEN_DECODE_FAILED));
}
// Destructure the decoded token payload
const {
uuid = '',
iat = 0,
exp = 0,
purpouse,
role,
} = (decoded as TokenPayload) ?? {};
// Ensure that the required fields are present in the token
if (!uuid || !iat || !exp)
return reject(new Error(errors.TOKEN_DECODE_FAILED));
// Verify that the token's purpose matches the required purpose
if (purpouse !== reqPurpouse)
return reject(new Error(errors.TOKEN_INVALID));
// Resolve the promise with the decoded token payload
return resolve({ uuid, iat, exp, purpouse, role });
}
);
});
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.