Tokens are essential components of modern web application security. They are used to authenticate and authorize access to protected resources, such as APIs or databases, and ensure that only authorized users can access them. In this article, we will provide a thorough description of what tokens are and the many sorts of tokens, such as access tokens, refresh tokens, and more.
What are Tokens?
Tokens are digital credentials that allow users and applications to be authenticated and authorized. A token is a string of characters that indicates a specific permission grant and often comprises information about the person or client who receives the token. Tokens can be used to gain access to restricted resources such as APIs or web services, as well as to validate the user's or client's identity.
Types of Tokens
1. Access Token
An access token is a bearer token that is used to grant access to a protected resource. It is typically issued by an authentication server or identity provider and contains information about the user and their permissions.
An authentication server can issue JWTs as access tokens that contain information about the user and the permissions granted to the client in order to authenticate users and grant access to protected resources.
The access token is then presented to the resource server whenever the user tries to access a protected resource and is used to verify that the user is authorized to access the resource.
Access tokens are short-lived and expire after a certain amount of time, typically an hour or less. This ensures that even if an access token is compromised, it can only be used for a limited amount of time before it becomes invalid. Additionally, access tokens can be revoked at any time by the authentication server or the user, which makes them more secure than long-lived tokens.
2. Refresh Token
Refresh tokens are long-lived tokens that are used to obtain a new access token. They are typically issued along with an access token and can be used to request a new access token when the current one expires. Refresh tokens are more secure than storing credentials on a device or browser, as they can be revoked by the authentication server at any time.
Refresh tokens are usually kept separate from access tokens and are only used to obtain new access tokens. They are not passed along with API requests or used to authenticate users directly. This ensures that even if a refresh token is compromised, the attacker cannot use it to access protected resources directly.
3. ID Token
ID tokens are tokens that are used to authenticate the user and provide information about the user's identity. ID tokens are typically used in scenarios where a user logs in with an external identity provider (e.g., Google or Facebook). The ID token contains information about the user, such as their name and email address, and can be used by the client to authenticate the user.
4. Session Token
Session tokens are tokens that are used to maintain a user's session with a web application. When a user logs in to a web application, the server issues a session token that is used to identify the user's session. The session token is typically stored in a cookie on the user's computer and is used to authenticate the user on subsequent requests.
Note: There are various sorts of tokens used in modern authentication servers; the ones listed above are some of the most common.
Using Access Tokens and Refresh Tokens
A simple illustration of a Node.js application using JavaScript to show how access tokens and refresh tokens work.
Prep work
// Create a directory
mkdir desktop/access_token_refresh_token
// Change the directory into the folder
cd access_token_refresh_token
// Initialize npm
npm init -y
Install the required dependency for this project with the command below
npm install jsonwebtoken express
Because this is a simple app, we'll make two files: index.js will handle all of the app's logic, and auth.middleware.js will handle token authorization and checking.
touch index.js
touch auth.middleware.js
Once that is created let us create the logic for the app
// Index.js
import express from 'express';
import jwt from 'jsonwebtoken';
import { authenticateToken } from './auth.middleware.js';
const app = express();
app.use(express.json());
// Generate a secret key for signing JWTs (normally this would be a long and random string)
const secret = 'very_Secure_secret_key';
const refresh_token_1 = "enter user1 refresh token here";
// In a real application, this data would be stored in a database
const users = [
{ id: 1, username: 'user1', password: 'password1' },
{ id: 2, username: 'user2', password: 'password2' },
];
// Index route
app.get('/', (_req, res) => {
return res.status(200).send({ message: "access_token_refresh_token"});
});
// Route for logging in and generating tokens
app.post('/login', (req, res) => {
// In a real application, this would be validated against a database
const user = users.find(u => u.username === req.body.username && u.password === req.body.password);
if (user) {
// Generate an access token with a short expiry time (e.g. 10 minutes)
const accessToken = jwt.sign({ userId: user.id }, secret, { expiresIn: '1m' });
// Generate a refresh token with a long expiry time (e.g. 30 days)
const refreshToken = jwt.sign({ userId: user.id }, secret, { expiresIn: '30d' });
res.json({ accessToken, refreshToken });
} else {
res.status(401).json({ error: 'Invalid username or password' });
}
});
// In a real application, this would retrieve data from a database based on the user id
app.get('/protected', authenticateToken, (_req, res) => {
return res.status(200).send({ message: "This is the protected message "});
})
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
he code above consists of two main parts: a login route and a protected route. When the server starts, it generates a secret key that will be used to sign and verify JWTs. In a real-world application, this key would be kept secret and not hardcoded in the source code.
The users
array contains some hardcoded user data that will be used to validate the login credentials. Again, in a real-world application, this data would typically be stored in a database.
The root route just returns a simple JSON message that says "access_token_refresh_token" and is used for testing purposes.
The /login
route is where the user logs in and receives both an access token and a refresh token. The user's credentials are validated against the users
array, and if they are valid, an access token and a refresh token are generated. The access token has a short expiry time of 1 minute, while the refresh token has a longer expiry time of 30 days. The tokens are signed using the secret key and returned to the client in a JSON response.
The /protected
route is where the user can access a protected resource. This route is protected by the authenticateToken
middleware function, which checks the validity of the user's access token. If the access token is valid, the user is allowed to access the protected resource.
When the client sends a request to the /protected
route, it includes the access token in the Authorization
header. The authenticateToken
middleware function extracts the token from the header, verifies its validity using the secret key, and if the token is valid, allows the request to proceed to the protected route. If the token is not valid, the middleware function returns a 401 Unauthorized status code.
// auth.middleware.js
import jwt from 'jsonwebtoken';
const secret = 'very_Secure_secret_key';
export function authenticateToken(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token not provided' });
}
jwt.verify(token, secret, (err, payload) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired access token' });
}
// Attach the user ID to the request object for use in subsequent handlers
req.id = payload.id;
next();
});
}
If the access token expires, the client can use the refresh token to obtain a new access token without having to log in again. In a real-world application, this would typically involve sending the refresh token to the server in a separate request, which would then generate a new access token if the refresh token is still valid. However, this functionality is not implemented in the code above.
Let us now use the refresh token that was provided. You will see that the accessToken is configured to expire in 1 minute (intentionally) even if the optimal length in a real-world application exceeds that. When the accessToken expires, the user will no longer be able to request that route, which is where the /refresh
the route comes in.
// Route for refreshing tokens
app.post('/refresh', (req, res) => {
const refreshToken = req.body.refreshToken;
// Verify that the refresh token is valid and retrieve the user ID from it
jwt.verify(refreshToken, secret, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
const userId = decoded.id;
// Check if the refresh token is in the database and has not expired
// This would typically be done using a database query
const refreshTokens = [
{ userId: 1, refreshToken: 'refresh_token_1', expiry: Date.now() + 30 * 24 * 60 * 60 * 1000 },
{ userId: 2, refreshToken: 'refresh_token_2', expiry: Date.now() + 30 * 24 * 60 * 60 * 1000 },
];
const storedToken = refreshTokens.find((token) => token.refreshToken === refreshToken);
if (!storedToken || storedToken.userId !== userId || storedToken.expiry < Date.now()) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate a new access token with a short expiry time (e.g. 10 minutes)
const accessToken = jwt.sign({ userId }, secret, { expiresIn: '10m' });
// Generate a new refresh token with a long expiry time (e.g. 30 days)
const newRefreshToken = jwt.sign({ userId }, secret, { expiresIn: '30d' });
// Update the refresh token in the database with the new value and expiry time
storedToken.refreshToken = newRefreshToken;
storedToken.expiry = Date.now() + 30 * 24 * 60 * 60 * 1000;
// Return the new access token and refresh token to the client
res.json({ accessToken, refreshToken: newRefreshToken });
});
});
When a client sends a request to this route with a valid refresh token, the server generates a new access token and a new refresh token and returns them to the client.
The code starts by extracting the refresh token from the request body
const refreshToken = req.body.refreshToken;
It then verifies the refresh token by calling jwt.verify()
, this function checks if the token is valid and has not expired. If the token is invalid or has expired, the server returns a 401 response with an error message
jwt.verify(refreshToken, secret, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid refresh token passed.' });
}
// If the refresh token is valid, the server extracts the user ID from the decoded token
const userId = decoded.id;
});
The server then checks if the refresh token is stored in the database and has not expired. In this example, the refresh tokens are stored in an array of objects
const refreshTokens = [
{ userId: 1, refreshToken: refresh_token_1, expiry: Date.now() + 30 * 24 * 60 * 60 * 1000 }, // expires in 30 days
{ userId: 2, refreshToken: 'refresh_token_2', expiry: Date.now() + 30 * 24 * 60 * 60 * 1000 }, // expires in 30 days
];
const storedToken = refreshTokens.find((token) => token.refreshToken === refreshToken);
if (!storedToken || storedToken.userId !== userId || storedToken.expiry < Date.now()) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
The server searches the refreshTokens
array for a token that matches the refresh token provided by the client. If a matching token is found, the server checks if the user ID in the token matches the user ID extracted from the decoded token and if the token has not expired. If any of these checks fail, the server returns a 401 response with an error message. If the refresh token is valid and has not expired, the server generates a new access token and a new refresh token and returns them to the client
const accessToken = jwt.sign({ userId }, secret, { expiresIn: '10m' });
const newRefreshToken = jwt.sign({ userId }, secret, { expiresIn: '30d' });
storedToken.refreshToken = newRefreshToken;
storedToken.expiry = Date.now() + 30 * 24 * 60 * 60 * 1000;
res.json({ "accessToken": accessToken, "refreshToken": newRefreshToken });
The server calls jwt.sign()
to generate a new access token and a new refresh token with short and long expiry times, respectively. It then updates the refresh token in the database with the new value and expiry time, and returns the new access token and refresh token to the client in a JSON response.
Wrapping Up
Access tokens and refresh tokens are essential components of modern web applications that require user authentication. A well-designed token-based authentication system can help prevent unwanted access to restricted resources while also providing an efficient approach to managing user authentication across numerous devices and applications. By effectively implementing token-based authentication, developers can protect the security and privacy of user data while providing a seamless and streamlined user experience.
Top comments (0)