Introduction to Authentication and Authorization using JSON Web Tokens in Nodejs
Tonie
Posted on May 11, 2023
As someone who’s been deeply involved with web applications, I can’t emphasize enough how critical they’ve become in our daily lives. Consequently, they demand reliable authentication and authorization mechanisms to safeguard sensitive information from prying eyes. In my opinion, JSON Web Tokens (JWT) offer a refreshingly simple yet secure way to implement these mechanisms in Node.js applications. In this article, I’ll provide you with a detailed guide to JWT-based authentication and authorization in Node.js, complete with a step-by-step walkthrough, helpful code snippets, and best practices you can follow.
What are JSON Web Tokens (JWTs)?
JSON Web Tokens (JWTs) are compact, URL-safe means of representing claims to be transferred between two parties. JWTs consist of three parts: a header, a payload, and a signature. The header contains metadata about the token, such as the algorithm used to sign it. The payload contains claims, which are statements about an entity (typically, the user) and additional data. The signature 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.
JWTs provide several advantages over traditional session-based authentication. Firstly, they don’t require storing user information on the server, which makes them highly scalable and easy to use. Secondly, they are stateless, which means that there is no need to manage server-side sessions. Thirdly, they can be easily exchanged between different systems and applications.
Overview of JWT structure
JSON Web Tokens (JWTs) are a type of token that consists of three parts: a header, a payload, and a signature. JWTs are encoded in Base64 URL format, which makes them easy to transmit over HTTP.
The header of a JWT contains information about the token, such as the algorithm used for the signature. The header is typically a JSON object that looks like this:
{
"alg": "HS256",
"typ": "JWT"
}
The alg
property specifies the algorithm used for the signature, which can be HMAC, RSA, or ECDSA. The typ
property specifies the type of token, which is always “JWT” for JWTs.
The payload of a JWT contains the claims, or pieces of information, about the user or client. The payload is also a JSON object that looks like this:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
The sub
property specifies the subject of the token, which is typically the user ID. The name
property specifies the name of the user. The iat
property specifies the time at which the token was issued, in Unix timestamp format.
Finally, the signature of a JWT is used to verify the integrity of the token. The signature is calculated by combining the header and payload of the token with a secret key, and then encoding the result using the algorithm specified in the header.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
) secret base64 encoded
JWT claims and their significance
JWT claims are pieces of information about the user or client that are included in the token’s payload. Claims can be used to provide context about the user or client, such as their ID or role, and can be used for both authentication and authorization purposes.
There are three types of claims in a JWT:
- Registered claims – These are a set of predefined claims that are commonly used in JWTs. Some examples of registered claims include iss (issuer), exp (expiration time), and nbf (not before). Registered claims are optional, but using them can make your JWTs more interoperable with other systems.
- Public claims – These are claims that are defined by the user and are intended to be shared with others. Public claims should follow the naming conventions specified in the JWT specification.
- Private claims – These are claims that are specific to your application and are not intended to be shared with others. Private claims should use a namespace that is specific to your application, to avoid conflicts with other claims.
Uses of JWT Claims
JWT claims can be used for various purposes in your application, including:
- Authentication – JWTs can be used to authenticate users by including a sub claim in the payload that identifies the user. When a user logs in, you can create a JWT with the user’s ID as the sub claim, and then use the JWT to authenticate the user on subsequent requests.
- Authorization – JWTs can be used to authorize users by including a roles claim in the payload that specifies the user’s roles or permissions. When a user makes a request, you can verify their JWT and check whether the roles claim includes the required role for the requested action.
- Statelessness – Because JWTs contain all the necessary information about the user or client, they can be used to create stateless APIs. This means that you don’t need to store session data on the server, which can improve scalability and reduce complexity.
Benefits of JWT
- Stateless: JWTs are stateless, meaning that the server does not need to keep track of sessions or tokens. This makes JWTs easier to scale and more efficient to use in distributed systems.
- Cross-domain: JWTs can be used across different domains and servers, making them a flexible solution for authentication and authorization in modern web applications.
- Security: JWTs are digitally signed using a secret key, making it difficult for an attacker to forge or tamper with the token. Additionally, JWTs can be encrypted to provide an extra layer of security.
- Decentralized: JWTs can be generated and verified by different services, allowing for decentralized authentication and authorization in microservices architectures.
- Payload: JWTs can contain a payload of user data, such as user ID or user permissions, making them useful for authorization as well as authentication.
Authentication with JWT in Node.js
Authentication is the process of verifying the identity of a user or a client trying to access a resource or service. In Node.js, authentication is typically implemented by requiring users to provide a valid set of credentials, such as a username and password, to gain access to the application. The server then checks the provided credentials against a database or a third-party authentication service, and if the credentials are valid, the user is granted access.
When a user or client logs in to a Node.js web application, the server generates a JWT and sends it back to the client. This JWT contains a set of claims, which are pieces of information about the user or client, such as their ID or role. The JWT is then stored on the client side, typically in a cookie or in local storage.
The authentication process using JWT involves three steps: (1) the client sends a login request with their credentials, (2) the server verifies the credentials and generates a JWT, and (3) the server sends the JWT to the client, which can then use it to access protected resources.
To implement JWT-based authentication in Node.js, you can use the “jsonwebtoken” library. Here’s a step-by-step guide:
- Install the “jsonwebtoken” library using npm.
`npm install jsonwebtoken`
js
- Create a login route in your Node.js application that accepts user credentials (e.g., username and password).
- Validate the credentials and generate a JWT using the “jsonwebtoken” library.
- Send the JWT back to the client as a response.
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Check if the user exists and the password is correct
const user = users.find(user => user.username === username && user.password === password);
if (!user) {
return res.status(401).json({ message: 'Invalid username or password' });
}
// Generate a JWT with a secret key
const payload = { username: user.username };
const secretKey = 'mySecretKey';
const options = { expiresIn: '1h' };
const token = jwt.sign(payload, secretKey, options);
// Return the token to the client
res.status(200).json({ token });
});
In the example above, we create a login
route that accepts a username and password from the client. We then check if the user exists and the password is correct. If the credentials are valid, we generate a JWT using the “jsonwebtoken” library and send it back to the client as a response.
Authorization with JWT in Node.js
Authorization, on the other hand, is the process of determining whether a user or a client has the necessary permissions to access a specific resource or perform a specific action in the application. In Node.js, authorization is typically implemented by assigning roles or permissions to users based on their identity or other attributes. For example, a user with an “admin” role may be authorized to access and modify certain parts of the application, while a regular user may only be authorized to view certain resources.
Authorization in the context of JWTs is often implemented by including user or client permissions as claims in the JWT. For example, a user with an “admin” role may have an “admin” claim in their JWT, which grants them access to certain parts of the application that regular users cannot access. The server can then check the presence of the “admin” claim in the JWT to determine whether the user is authorized to perform a specific action or access a specific resource.
Once a client has a valid JWT, they can use it to access protected resources on the server. To implement JWT-based authorization in Node.js, you can use middleware functions that verify the JWT and check the user’s permissions before allowing access to the protected resource.
Here’s a step-by-step guide for implementing JWT-based authorization in Node:
- Create a middleware function that verifies the JWT and check the user’s permissions before allowing access to the protected resource.
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ message: 'Invalid token' });
}
req.user = decoded;
next();
});
};
app.get('/protected', authMiddleware, (req, res) => {
res.status(200).json({ message: 'You are authorized to access this resource' });
});
In the example above, we create a middleware function that verifies the JWT and sets the decoded payload as the req.user
object. We then use this middleware function to protect the /protected
route.
When a client tries to access the /protected
route, the middleware function will verify the JWT in the “Authorization” header. If the JWT is valid, the middleware function will set the req.user
object and allow the client to access the protected resource. If the JWT is invalid or missing, the middleware function will return an error response.
Token Refresh and Expiry
One of the key features of JWTs is their ability to set an expiration time for the token. This provides an added layer of security by ensuring that a token cannot be used indefinitely. When a token expires, the user must obtain a new token to continue accessing protected resources.
In addition to setting an expiration time, JWTs can also include a refresh token. A refresh token is a separate token that is used to obtain a new JWT when the original token has expired. This can provide a better user experience by allowing users to continue accessing protected resources without having to constantly log in.
To implement token refresh and expiry in your Node.js application, you can use a combination of middleware and token management libraries. Here’s an example of how to implement token refresh and expiry using the jsonwebtoken library:
const jwt = require('jsonwebtoken');
const refreshTokenSecret = 'your_refresh_token_secret';
const accessTokenSecret = 'your_access_token_secret';
const refreshTokens = [];
// Middleware to check if user is authenticated
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
jwt.verify(token, accessTokenSecret, (err, user) => {
if (err) {
return res.sendStatus(403);
}
req.user = user;
next();
});
} else {
res.sendStatus(401);
}
};
// Endpoint to generate new access and refresh tokens
app.post('/token', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken || !refreshTokens.includes(refreshToken)) {
return res.sendStatus(403);
}
jwt.verify(refreshToken, refreshTokenSecret, (err, user) => {
if (err) {
return res.sendStatus(403);
}
const accessToken = jwt.sign({ username: user.username }, accessTokenSecret, { expiresIn: '15m' });
res.json({ accessToken });
});
});
// Endpoint to log in and generate access and refresh tokens
app.post('/login', (req, res) => {
// authenticate user
const username = req.body.username;
const user = { username: username };
const accessToken = jwt.sign(user, accessTokenSecret, { expiresIn: '15m' });
const refreshToken = jwt.sign(user, refreshTokenSecret);
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
// Endpoint to log out and revoke refresh token
app.post('/logout', (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.sendStatus(400);
}
const index = refreshTokens.indexOf(refreshToken);
if (index !== -1) {
refreshTokens.splice(index, 1);
}
res.sendStatus(204);
});
In this example, we’re using two JWT secrets: one for access tokens and one for refresh tokens. We’re also storing refresh tokens in an array on the server, so that we can check whether a given refresh token is valid.
The authenticateJWT
middleware checks whether a user is authenticated by verifying the access token. If the token is valid, it sets the req.user
property to the user object.
The /token
endpoint is used to generate new access tokens using a valid refresh token. If the refresh token is valid, we verify it and generate a new access token with a 15 minute expiration time.
The /login
endpoint is used to log in and generate a new access token and refresh token pair. We generate both tokens and store the refresh token in the refreshTokens array
Best Practices for JWT-based Authentication and Authorization
Here are some best practices to follow when implementing JWT-based authentication and authorization in Node.js:
- Always use a secure random secret key for signing and verifying JWTs.
- Use short-lived JWTs (e.g., one hour) to minimize the risk of token hijacking.
- Implement rate limiting and other security measures to prevent brute force attacks on the login endpoint.
- Use HTTPS to encrypt all communication between the client and the server.
- Store sensitive user data (e.g., passwords) securely using hashing and salting.
- Use a consistent naming convention for JWT-related headers and payload fields.
- Use middleware functions to validate JWTs and protect resources.
Conclusion
JSON Web Tokens (JWTs) provide a secure and efficient way to implement authentication and authorization in Node.js applications. By following the best practices outlined in this article, you can ensure that your JWT-based authentication and authorization system is robust and secure.
Posted on May 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.