DEV Community

romeus clarens
romeus clarens

Posted on

Secure and reliable cookie-based authentication

Protecting Cookie-Based Authentication Against CSRF Attacks

Cookie-based authentication is one of the most secure ways to manage authentication in a web application.

However, when cookies are configured incorrectly, the application may become vulnerable to CSRF attacks.

CSRF stands for Cross-Site Request Forgery.

A CSRF attack happens when another website tricks a logged-in user’s browser into sending an unwanted request to your application.

How Cookie Authentication Works

After the user logs in, the server creates a session.

The session is stored securely on the server, usually in Redis.

The server sends a session token to the browser as a cookie.

Set-Cookie: session_token=abc123;
HttpOnly;
Secure;
SameSite=Lax;
Path=/
Enter fullscreen mode Exit fullscreen mode

The browser automatically sends this cookie with future requests.

GET /api/profile
Cookie: session_token=abc123
Enter fullscreen mode Exit fullscreen mode

The server uses the token to find and validate the user’s session.

Why CSRF Is Possible

The browser automatically sends cookies.

This happens even when a request is initiated from another website, depending on the cookie configuration.

Imagine that a user is logged into:

https://mybank.com
Enter fullscreen mode Exit fullscreen mode

The user then visits a malicious website containing this form:

<form
  action="https://mybank.com/api/transfer"
  method="POST"
>
  <input
    type="hidden"
    name="recipient"
    value="attacker"
  />

  <input
    type="hidden"
    name="amount"
    value="1000"
  />
</form>

<script>
  document.forms[0].submit();
</script>
Enter fullscreen mode Exit fullscreen mode

The malicious website submits the form automatically.

The browser may attach the user’s banking cookie.

The bank could believe that the request came from the authenticated user.

This is why cookie-based authentication needs CSRF protection.

Use HttpOnly Cookies

Your authentication cookie should use HttpOnly.

{
  httpOnly: true
}
Enter fullscreen mode Exit fullscreen mode

This prevents frontend JavaScript from reading the session token.

For example, the following code cannot access an HttpOnly session cookie:

console.log(document.cookie);
Enter fullscreen mode Exit fullscreen mode

This helps protect the session token from being stolen through malicious JavaScript.

However, HttpOnly alone does not prevent CSRF.

The browser can still automatically send the cookie.

Use Secure Cookies

In production, always enable Secure.

{
  secure: true
}
Enter fullscreen mode Exit fullscreen mode

A secure cookie is sent only through HTTPS.

The browser will not send it over an unencrypted HTTP connection.

A complete production cookie could look like this:

const cookieOptions = {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/',
};
Enter fullscreen mode Exit fullscreen mode

During local development, you can conditionally disable Secure:

const cookieOptions = {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax' as const,
  path: '/',
};
Enter fullscreen mode Exit fullscreen mode

Use SameSite Cookies

The SameSite option controls when a browser sends cookies across websites.

For most applications, use:

{
  sameSite: 'lax'
}
Enter fullscreen mode Exit fullscreen mode

SameSite=Lax blocks cookies from being included in many cross-site requests.

For applications requiring stronger restrictions, use:

{
  sameSite: 'strict'
}
Enter fullscreen mode Exit fullscreen mode

SameSite=Strict provides stronger protection but may interfere with some login and navigation flows.

Use SameSite=None only when your architecture truly requires cross-site cookies.

{
  sameSite: 'none',
  secure: true
}
Enter fullscreen mode Exit fullscreen mode

When using SameSite=None, explicit CSRF-token protection becomes extremely important.

Use an Explicit CSRF Token

A CSRF token is a random value generated by the server.

The legitimate frontend requests this token.

It then sends the token inside a custom header for every state-changing request.

X-CSRF-Token: generated-token
Enter fullscreen mode Exit fullscreen mode

A malicious website may cause the browser to send cookies.

However, it cannot normally read the valid CSRF token and add it to the custom header.

The server rejects any request that does not contain the correct token.

Install CSRF Protection in NestJS

Install the required packages:

npm install csrf-csrf cookie-parser
npm install -D @types/cookie-parser
Enter fullscreen mode Exit fullscreen mode

Generate a strong secret:

openssl rand -base64 32
Enter fullscreen mode Exit fullscreen mode

Add it to your .env file:

CSRF_SECRET="your-generated-secret"
Enter fullscreen mode Exit fullscreen mode

Never publish or commit this secret to Git.

Configure CSRF Protection

Create src/security/csrf.ts:

import { doubleCsrf } from 'csrf-csrf';
import type { Request } from 'express';

const isProduction =
  process.env.NODE_ENV === 'production';

export const {
  generateCsrfToken,
  doubleCsrfProtection,
} = doubleCsrf({
  getSecret: () => {
    const secret = process.env.CSRF_SECRET;

    if (!secret) {
      throw new Error('CSRF_SECRET is missing');
    }

    return secret;
  },

  getSessionIdentifier: (request: Request) => {
    return (
      request.cookies.session_token ??
      'anonymous'
    );
  },

  cookieName: isProduction
    ? '__Host-csrf'
    : 'csrf',

  cookieOptions: {
    httpOnly: true,
    secure: isProduction,
    sameSite: 'lax',
    path: '/',
  },

  getCsrfTokenFromRequest: (request: Request) => {
    return request.headers[
      'x-csrf-token'
    ] as string;
  },
});
Enter fullscreen mode Exit fullscreen mode

Enable the Middleware

Update main.ts:

import { NestFactory } from '@nestjs/core';
import cookieParser from 'cookie-parser';
import { AppModule } from './app.module';
import {
  doubleCsrfProtection,
} from './security/csrf';

async function bootstrap() {
  const app =
    await NestFactory.create(AppModule);

  app.use(cookieParser());

  app.enableCors({
    origin: 'https://app.example.com',
    credentials: true,
    allowedHeaders: [
      'Content-Type',
      'X-CSRF-Token',
    ],
  });

  app.use(doubleCsrfProtection);

  await app.listen(4000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

cookieParser() must be registered before the CSRF middleware.

Create a CSRF Token Endpoint

import {
  Controller,
  Get,
  Req,
  Res,
} from '@nestjs/common';

import type {
  Request,
  Response,
} from 'express';

import {
  generateCsrfToken,
} from './security/csrf';

@Controller('csrf-token')
export class CsrfController {
  @Get()
  getToken(
    @Req() request: Request,
    @Res() response: Response,
  ) {
    const csrfToken =
      generateCsrfToken(request, response);

    return response.json({
      csrfToken,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Retrieve the Token from React

const response = await fetch(
  'https://api.example.com/csrf-token',
  {
    credentials: 'include',
  },
);

const { csrfToken } =
  await response.json();
Enter fullscreen mode Exit fullscreen mode

Keep the CSRF token temporarily in memory.

Send the Token with Protected Requests

await fetch(
  'https://api.example.com/profile',
  {
    method: 'PATCH',

    credentials: 'include',

    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken,
    },

    body: JSON.stringify({
      name: 'Clarens',
    }),
  },
);
Enter fullscreen mode Exit fullscreen mode

The browser sends the authentication cookie automatically.

The frontend sends the CSRF token explicitly.

The server verifies both values before accepting the request.

Without the correct CSRF token, the server returns:

403 Forbidden
Enter fullscreen mode Exit fullscreen mode

Final Secure Cookie Configuration

For production, your session cookie should look similar to this:

response.cookie(
  'session_token',
  sessionToken,
  {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 7 * 24 * 60 * 60 * 1000,
  },
);
Enter fullscreen mode Exit fullscreen mode

The complete protection is:

Server-side session
        +
HttpOnly cookie
        +
Secure cookie
        +
SameSite restriction
        +
Explicit CSRF token
Enter fullscreen mode Exit fullscreen mode

This architecture allows developers to use cookie-based authentication while protecting users against forged cross-site requests.

Top comments (0)