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=/
The browser automatically sends this cookie with future requests.
GET /api/profile
Cookie: session_token=abc123
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
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>
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
}
This prevents frontend JavaScript from reading the session token.
For example, the following code cannot access an HttpOnly session cookie:
console.log(document.cookie);
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
}
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: '/',
};
During local development, you can conditionally disable Secure:
const cookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/',
};
Use SameSite Cookies
The SameSite option controls when a browser sends cookies across websites.
For most applications, use:
{
sameSite: 'lax'
}
SameSite=Lax blocks cookies from being included in many cross-site requests.
For applications requiring stronger restrictions, use:
{
sameSite: 'strict'
}
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
}
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
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
Generate a strong secret:
openssl rand -base64 32
Add it to your .env file:
CSRF_SECRET="your-generated-secret"
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;
},
});
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();
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,
});
}
}
Retrieve the Token from React
const response = await fetch(
'https://api.example.com/csrf-token',
{
credentials: 'include',
},
);
const { csrfToken } =
await response.json();
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',
}),
},
);
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
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,
},
);
The complete protection is:
Server-side session
+
HttpOnly cookie
+
Secure cookie
+
SameSite restriction
+
Explicit CSRF token
This architecture allows developers to use cookie-based authentication while protecting users against forged cross-site requests.
Top comments (0)