How to make your APIs more secure using Access tokens and Refresh tokens

How to make your APIs more secure using Access tokens and Refresh tokens

While creating a secure backend for various protected routes, it is important to implement authentication and authorization, which are most commonly done using JWT tokens.

What is a JWT token?

JWT stands for JSON Web Tokens, which are commonly used to build a secure backend with Node.js. JWT encodes data into a string using a secret key and allows us to verify if the string has been tampered with by using the same secret key. This ensures that the token being received is authentic and helps protect our routes.

const token = await jwt.sign(userData, "YOUR_SECRET_KEY")

Access Token:

An access token is a long string that is used to authenticate the request that is coming from the front end. It carries user information like username, email, etc. They are generally short-lived tokens which gets expired quickly as compared to refresh tokens.

Refresh token:

An access token is a long string used to authenticate requests coming from the frontend. It carries user information, such as the username, email, etc. Access tokens are generally short-lived and expire quickly compared to refresh tokens.

Using the Access token and Refresh token together:

We will first create a backend with three major routes: Signup, Protected, and Refresh.

Signup Route:

The user will send their credentials, and we will create an account for them, as well as generate an access token and a refresh token. When the user signs up, the access token and the refresh token (stored in cookies) will be sent to them.

app.post('/signup', (req, res) => {
  const user = createNewUser(req.body); // Fucntion to craete the user in the database

  // Create Access Token
  const accessToken = jwt.sign(user, SECRET_KEY, { expiresIn: '15m' });

  // Create Refresh Token
  const refreshToken = jwt.sign(user, REFRESH_SECRET_KEY, { expiresIn: '7d' });

  res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, maxAge: 7 * 24 * 60 * 60 * 1000 }); // 7 days
  res.json({ accessToken });
});

Whenever users request protected routes, the access token and refresh token are sent along with the request. If the access token is not valid, the user will not be able to access the route, and the backend will send an Expired token error to the frontend.

Protected Route:

app.get('/protected', (req, res) => {
  const token = req.headers['access_token']; // Assuming Bearer token in header

  if (!token) {
    return res.status(403).json({ error: 'Access token required' });
  }

  try {
    // Verify the token
    const decoded = jwt.verify(token, SECRET_KEY);

    // If verification is successful, continue with the protected resource
    res.json({ message: 'Access granted' });
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      // Token expired error handling
      return res.status(401).json({ error: 'Token expired' });
    } else if (err.name === 'JsonWebTokenError') {
      // Invalid token error handling
      return res.status(401).json({ error: 'Invalid or tampered token' });
    } else {
      // Other errors
      return res.status(500).json({ error: 'Internal server error' });
    }
  }
});

Now, the frontend will receive error messages from the backend if authentication fails. If the token has expired, the frontend will send a request to the backend to renew the access token using the refresh token. This request will be made to the Refresh Route.

Refresh Route:

The refresh route verifies the refresh token, and if the refresh token is valid then the user receives the updated access token.

app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken; // Get the refresh token from the cookie

  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }

  try {
    // Verify the refresh token
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET_KEY);

    // If verification is successful, generate a new access token
    const newAccessToken = jwt.sign(userData, SECRET_KEY, { expiresIn: '15m' });

    // Return the new access token
    res.json({ accessToken: newAccessToken });
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      // Refresh token expired error handling
      return res.status(401).json({ error: 'Refresh token expired' });
    } else if (err.name === 'JsonWebTokenError') {
      // Invalid refresh token error handling
      return res.status(401).json({ error: 'Invalid refresh token' });
    } else {
      // Other errors
      return res.status(500).json({ error: 'Internal server error' });
    }
  }
});