Using the State Parameter to Prevent CSRF Attacks
How the OAuth 2.0 state parameter prevents cross-site request forgery attacks and login CSRF. Implementation guide with examples.
Detailed Explanation
The State Parameter and CSRF Protection
The state parameter is a critical security measure in OAuth 2.0 that prevents Cross-Site Request Forgery (CSRF) attacks during the authorization flow. Without it, an attacker can trick a user into authorizing the attacker's account in the victim's session.
The Attack Without State
- The attacker starts an OAuth flow with the victim application and gets an authorization code
- The attacker constructs a URL:
https://victim-app.com/callback?code=ATTACKER_CODE - The attacker tricks the victim into clicking the link (via email, forum post, etc.)
- The victim's browser sends the request to the victim app
- The victim app exchanges the attacker's code for a token and links the attacker's external account to the victim's session
- The attacker now has access to the victim's account
How State Prevents This
- Before redirect: Generate a cryptographically random string and store it in the session:
const state = crypto.randomUUID();
sessionStorage.setItem("oauth_state", state);
// Include state in the authorization URL
- On callback: Verify the state matches:
const urlParams = new URLSearchParams(window.location.search);
const returnedState = urlParams.get("state");
const storedState = sessionStorage.getItem("oauth_state");
if (returnedState !== storedState) {
throw new Error("State mismatch — possible CSRF attack");
}
sessionStorage.removeItem("oauth_state");
// Proceed with token exchange
State Best Practices
| Practice | Reason |
|---|---|
Use crypto.randomUUID() or 32+ random bytes |
Prevent guessing |
| Store in session (not localStorage) | Expires with session |
| One-time use | Prevent replay attacks |
| Include in every authorization request | Defense in depth |
Advanced: Encoding Data in State
You can embed additional information (like the return URL) by encoding it:
const statePayload = JSON.stringify({
nonce: crypto.randomUUID(),
returnTo: "/dashboard",
});
const state = btoa(statePayload);
On callback, decode and verify the nonce, then redirect to the encoded returnTo URL.
Use Case
Any web application implementing the Authorization Code or Authorization Code + PKCE flow. The state parameter should be included in every authorization request to protect users from CSRF and login CSRF attacks.