Skip to main content

CSRF Protection

How SVA OAuth protects against Cross-Site Request Forgery (CSRF) attacks.

What is CSRF?

CSRF is an attack that forces a user to execute unwanted actions on a web application in which they're authenticated.

State Parameter

SVA OAuth uses the state parameter for CSRF protection:

  1. Generate State - Random token generated per OAuth flow
  2. Store in Session - State stored server-side in session
  3. Include in Request - State sent in authorization request
  4. Verify in Callback - State verified when callback is received

How It Works

Step 1: Generate State

import secrets

state = secrets.token_urlsafe(32)
request.session['sva_oauth_state'] = state

Step 2: Include in Authorization

GET /oauth/authorize?
client_id=...
&redirect_uri=...
&state={state}
...

Step 3: Verify in Callback

received_state = request.GET.get('state')
stored_state = request.session.get('sva_oauth_state')

if received_state != stored_state:
# CSRF attack detected
raise CSRFError()

# Clear state after verification
del request.session['sva_oauth_state']

Automatic Protection

The sva-oauth-client package automatically implements CSRF protection:

from sva_oauth_client.client import SVAOAuthClient

client = SVAOAuthClient(...)

# State is automatically generated and verified
auth_url, code_verifier = client.get_authorization_url()
# State is included in auth_url
# State is stored in session

Built-in Views

The package's built-in views handle CSRF protection automatically:

  • /oauth/login/ - Generates state
  • /oauth/callback/ - Verifies state
  • /oauth/exchange/ - Validates state

Manual Implementation

Generate State

import secrets

def generate_state(request):
"""Generate CSRF state token"""
state = secrets.token_urlsafe(32)
request.session['oauth_state'] = state
return state

Verify State

def verify_state(request, received_state):
"""Verify CSRF state token"""
stored_state = request.session.get('oauth_state')

if not stored_state:
return False

if received_state != stored_state:
return False

# Clear state after verification
del request.session['oauth_state']
return True

Security Properties

Randomness

State tokens are cryptographically random:

import secrets

# Cryptographically secure random token
state = secrets.token_urlsafe(32)

Unpredictability

State tokens are unpredictable:

  • 32 bytes of random data
  • Base64 URL-safe encoding
  • ~256 bits of entropy

Single Use

State tokens are single-use:

  • Verified once in callback
  • Cleared from session after verification
  • Cannot be reused

Best Practices

  1. Always use state - Never skip state parameter
  2. Store securely - State in server-side session
  3. Verify always - Always verify state in callback
  4. Clear after use - Remove state after verification
  5. Generate randomly - Use cryptographically secure random

Common Mistakes

❌ Skipping State

# ❌ Bad - No state parameter
auth_url = f"{base_url}/oauth/authorize?client_id=..."

❌ Client-Side State

# ❌ Bad - State stored client-side
state = generate_state()
localStorage.setItem('oauth_state', state)

❌ Not Verifying

# ❌ Bad - Not verifying state
code = request.GET.get('code')
# Missing state verification

✅ Correct Implementation

# ✅ Good - State generated and verified
state = secrets.token_urlsafe(32)
request.session['oauth_state'] = state

# In callback
received_state = request.GET.get('state')
stored_state = request.session.get('oauth_state')

if received_state != stored_state:
raise CSRFError()

Testing

Test CSRF Protection

# tests.py
from django.test import TestCase, Client

class CSRFTestCase(TestCase):
def test_state_verification(self):
"""Test state parameter verification"""
client = Client()

# Generate state
response = client.get('/oauth/login/')
state = client.session.get('sva_oauth_state')
self.assertIsNotNone(state)

# Verify with correct state
response = client.get(f'/oauth/callback/?code=test&state={state}')
# Should succeed

# Verify with incorrect state
response = client.get('/oauth/callback/?code=test&state=wrong')
# Should fail

Next Steps