CSRF is a type of web security vulnerability where an attacker tricks a user’s browser into making an unauthorized request to a different site where the user is already authenticated. The goal of CSRF protection is to prevent attackers from forging actions on behalf of a user without their consent.

A simple scenario

  • You’re logged into yourbank.com.
  • You visit a malicious site.
  • That site makes a request to yourbank.com/transfer?to=attacker&amount=1000.
  • Your browser sends your bank cookies automatically, so the request is accepted as coming from you.

Preventing CSRF in OAuth 2.0

In OAuth 2.0, CSRF is prevented by:

  1. Using a state parameter, a random string sent during the login request.
  2. When the user is redirected back to your app, you check that the returned state matches what you originally sent.
  3. If it doesn’t, the request is rejected.

A concrete example

Let’s see a concrete code example. For simplicity, I’ll assume we’re want to authenticate with Google OAuth 2.0 flow.

User starts login from a frontend app

// Generate a string that is cryptographically random, not something predictable like a timestamp.
const state = crypto.randomUUID();
 
// Store state in localStorage.
// In server-rendered apps, store the state in the session server-side instead.
localStorage.setItem('oauth_state', state);
 
const clientId = 'YOUR_GOOGLE_CLIENT_ID';
const redirectUri = 'https://yourapp.com/oauth2callback';
const scope = 'openid email profile';
 
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?` +
  `client_id=${clientId}` +
  `&redirect_uri=${redirectUri}` +
  `&response_type=code` +
  `&scope=${scope}` +
  `&state=${state}`;

User is redirected back to your app

After login and consent, Google redirects the user back with:

https://yourapp.com/oauth2callback?code=abcd1234&state=b70c9c34-ff52-40df-8cf6-013f2c37cf7e

State parameter validation

const urlParams = new URLSearchParams(window.location.search);
const returnedState = urlParams.get('state');
const expectedState = localStorage.getItem('oauth_state');
 
if (returnedState !== expectedState) {
  alert('Invalid state. Login aborted.');
} else {
  const code = urlParams.get('code');
  // Send `code` to your backend
}