Add CSRF Protection to Backend

Protect your app by handling CSRF tokens in auth endpoints and middleware in the backend.

When using session cookies for authentication, you must protect against CSRF attacks. CSRF is a type of web security vulnerability where an attacker tricks a user into unknowingly submitting a request to a web application where they're already authenticated. Since browsers automatically include cookies with requests, the malicious request appears legitimate to the server. This allows attackers to perform actions on behalf of the user without their consent.

For modern browsers, CSRF attacks can be prevented by using the SameSite cookie attribute. Setting the SameSite cookie attribute toStrict will ensure that the browser won't send the cookie for cross-site requests, eliminating the threat of CSRF. However, for looser SameSite settings or older browsers that don't support the SameSite cookie attribute, you'll need to implement your own CSRF protection. One common approach for building CSRF protection is to use the Synchronizer Token pattern.

Below, we'll show how to use the Synchronizer Token pattern to prevent CSRF attacks. For a high-level overview of how this pattern works, please refer to the following documentation.


Add CSRF Utility Functions

First, create a couple of utility functions somewhere in your backend for use within the Callback Endpoint and the auth middleware.

// csrf.ts
import crypto from 'crypto';

/* Generates a new CSRF token. */
export const createCsrfToken = function () {
  return crypto.randomBytes(32).toString('hex');
};

/* Updates the CSRF cookie with the CSRF token. */
export const updateCsrfCookie = function (req, res) {
  const { csrfToken } = req.session;
  res.cookie('CSRF-TOKEN', csrfToken, {
    httpOnly: false, // Must be false so that your frontend JavaScript code can access the value.
    maxAge: 1800000, // This value should match your session cookie expiration, e.g. 30 mins
    path: '/',
    sameSite: true, // This should match the SameSite settings of your session cookie.
    secure: false, // IMPORTANT: Only set this to false if your server isn't using HTTPS.
  });
};




Update Callback Endpoint

Next, your Callback Endpoint needs to be updated to create a CSRF token. Once the CSRF token is generated, it will be stored in a CSRF cookie so that the frontend JavaScript code can read it. In addition, the CSRF token will be stored in the session cookie so the CSRF middleware can access and validate it.

// Callback endpoint
import { createCsrfToken, updateCsrfCookie } from './csrf';

app.get('/auth/callback', async (req, res, next) => {
  try {
    const callbackResult = await wristbandAuth.callback(req, res);
    const { callbackData, redirectUrl, type } = callbackResult;

    if (type === CallbackResultType.REDIRECT_REQUIRED) {
      return res.redirect(redirectUrl);
    }

    req.session.isAuthenticated = true;
    req.session.accessToken = callbackData.accessToken;
    req.session.expiresAt = Date.now() + callbackData.expiresIn * 1000;
    req.session.refreshToken = callbackData.refreshToken;
    req.session.userId = callbackData.userinfo.sub;
    req.session.tenantId = callbackData.userinfo.tnt_id;
    req.session.tenantDomainName = callbackData.tenantDomainName;
    req.session.tenantCustomDomain = callbackData.tenantCustomDomain || undefined;
    
    /* ***** BEGIN NEW CSRF LOGIC ***** */
    
    req.session.csrfToken = createCsrfToken();
    await req.session.save();
    updateCsrfCookie(req, res);
    
    /* ***** END NEW CSRF LOGIC ***** */

    return res.redirect(callbackData.returnUrl || '<replace_with_a_default_return_url>');
  } catch (err) {
    console.error(err);
    return next(err);
  }
});



Update Logout Endpoint

Now, update your Logout Endpoint to delete the CSRF cookie before redirecting to the Wristband Logout Endpoint.

// Logout endpoint
app.get('/auth/logout', async (req, res, next) => { 
  const { session } = req;
  const { refreshToken, tenantCustomDomain, tenantDomainName } = session;
  const logoutConfig = { refreshToken, tenantCustomDomain, tenantDomainName };
  res.clearCookie('session');
  
  /* ***** BEGIN NEW CSRF LOGIC ***** */
  
  res.clearCookie('CSRF-TOKEN');
  
  /* ***** END NEW CSRF LOGIC ***** */
  
  session.destroy();

  try {
    const logoutUrl = await wristbandAuth.logout(req, res, logoutConfig);
    return res.redirect(logoutUrl);
  } catch (err) {
    console.error(err);
    return next(err);
  }
});



Add CSRF Validation to Auth Middleware

Finally, update the auth middleware to check whether the CSRF token in the request header matches the CSRF token that comes from the session cookie. If the CSRF token verification fails, then a 403 response will be returned.

// auth-middleware.ts
import { updateCsrfCookie } from './csrf';

const authMiddleware = async function (req, res, next) {
  const { csrfToken, isAuthenticated } = req.session;
  if (!isAuthenticated) {
    return res.status(401).send();
  }
  
  /* ***** BEGIN NEW CSRF LOGIC ***** */
  
  // Validate that the CSRF token is valid.
  if (!csrfToken || csrfToken !== req.headers['x-csrf-token']) {
    return res.status(403).send();
  }
  
  // "Touch" the CSRF cookie to extend the expiration window.
  updateCsrfCookie(req, res);
  
  /* ***** END NEW CSRF LOGIC ***** */

  await req.session.save();
  return next();
};

export default authMiddleware;


What’s Next

Next, let's enhance the frontend to be able to pass a CSRF request header when making API calls to your backend.