Skip to content

OIDC Security — Protecting Against Replay Attacks, CSRF, and Token Theft

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about OIDC Security. We cover key concepts, practical examples, and best practices to help you master this topic.

OpenID Connect security relies on several mechanisms to protect against common attacks: the nonce parameter prevents token replay, the state parameter prevents CSRF, and proper token storage prevents credential theft.

What You'll Learn

  • How the nonce parameter prevents ID token replay attacks
  • How the state parameter prevents CSRF Attacks
  • Best practices for secure token storage and transmission

Why It Matters

An attacker who intercepts an ID token can replay it to impersonate a user. A CSRF attack can trick a user into linking their account to an attacker's session. Without proper OIDC security measures, your authentication system is vulnerable to these and other attacks.

Real-World Use

Doda Browser's OIDC implementation uses a cryptographically random nonce embedded in every authentication request. The backend verifies the nonce in the returned ID token, ensuring that even if a token is intercepted, it cannot be replayed. The state parameter is bound to the user's session, preventing CSRF attacks.

flowchart LR
    A["User"] -->|"Auth Request\n+ state + nonce"| B["OIDC Provider"]
    B -->|"ID Token\n+ state + nonce"| C["Your App"]
    C --> D{"Verify state\nmatches session?"}
    D -->|No| E["Reject\nCSRF detected"]
    D -->|Yes| F{"Verify nonce\nmatches request?"}
    F -->|No| G["Reject\nReplay detected"]
    F -->|Yes| H["Accept Token"]
    style E fill:#fecaca,stroke:#dc2626
    style G fill:#fecaca,stroke:#dc2626
    style H fill:#bbf7d0,stroke:#16a34a

Nonce-Based Replay Prevention

The nonce is a random value sent with the authentication request that is embedded in the resulting ID token:

import secrets
import jwt

# Generate a nonce for each authentication request
def create_auth_request(provider_config):
    nonce = secrets.token_urlsafe(32)
    state = secrets.token_urlsafe(16)

    # Store nonce and state in session
    session['oidc_nonce'] = nonce
    session['oidc_state'] = state

    params = {
        'client_id': provider_config['client_id'],
        'redirect_uri': provider_config['redirect_uri'],
        'response_type': 'code',
        'scope': 'openid profile email',
        'state': state,
        'nonce': nonce
    }
    return params

# Verify nonce in the returned ID token
def verify_id_token(encoded_token, expected_nonce, jwks, expected_issuer, expected_audience):
    payload = jwt.decode(
        encoded_token,
        jwks,
        algorithms=["RS256"],
        issuer=expected_issuer,
        audience=expected_audience
    )

    received_nonce = payload.get('nonce')
    if received_nonce != expected_nonce:
        raise ValueError("Nonce mismatch - possible replay attack")

    return payload

State Parameter CSRF Protection

The state parameter links the authorization response to the original request:

// Express middleware for OIDC state validation
function validateState(req, res, next) {
  const returnedState = req.query.state;
  const storedState = req.session.oidcState;

  if (!returnedState || returnedState !== storedState) {
    // CSRF attack detected
    return res.status(401).json({
      error: 'invalid_state',
      message: 'State parameter mismatch. Possible CSRF attack.'
    });
  }

  // Clear used state to prevent reuse
  delete req.session.oidcState;
  next();
}

Token Binding and Storage

Secure token storage is critical for preventing theft:

# Server-side: store tokens in HTTP-only, secure cookies
def set_token_cookies(response, id_token, access_token, refresh_token):
    response.set_cookie(
        'id_token',
        id_token,
        httponly=True,
        secure=True,
        samesite='Lax',
        max_age=3600,
        path='/'
    )
    response.set_cookie(
        'access_token',
        access_token,
        httponly=True,
        secure=True,
        samesite='Lax',
        max_age=3600,
        path='/'
    )
    # Refresh token: shorter lifetime, strict path
    if refresh_token:
        response.set_cookie(
            'refresh_token',
            refresh_token,
            httponly=True,
            secure=True,
            samesite='Strict',
            max_age=86400,
            path='/auth/refresh'
        )
    return response

Common Mistakes

1. Reusing Nonces

If the same nonce is used across multiple requests, a captured token can be replayed until the nonce changes. Generate a fresh nonce for every authentication request.

2. Not Checking the State Parameter

Without state validation, an attacker can craft a URL that authenticates the victim and redirects them to the attacker's session. Always validate state.

3. Storing Tokens in localStorage

localStorage is accessible to any JavaScript on the same origin. XSS Attacks can steal tokens. Use HTTP-only cookies instead.

4. Not Rotating Signing Keys

OIDC providers should rotate their JWKS signing keys periodically. Applications should handle key rotation gracefully by fetching fresh JWKS on key errors.

5. Accepting Tokens Over HTTP

ID tokens and access tokens must never be transmitted over unencrypted connections. Enforce HTTPS at every level.

Practice Questions

  1. What attack does the nonce parameter prevent?
  2. What attack does the state parameter prevent?
  3. Why should tokens be stored in HTTP-only cookies instead of localStorage?
  4. How often should signing keys be rotated?
  5. What should you do if ID token validation fails due to an unknown key?

Answers

  1. ID token replay attacks. 2. CSRF (Cross-Site Request Forgery). 3. HTTP-only cookies are not accessible to JavaScript, preventing XSS-based token theft. 4. Periodically, with a grace period for old keys. 5. Fetch the latest JWKS and retry validation.

Challenge

Build a complete OIDC security test suite that validates an implementation against the most common attacks: nonce replay, CSRF via state manipulation, token interception via open redirect, and JWKS key confusion attacks. Each test should demonstrate the attack and verify the defense.

FAQ

What is the nonce parameter in OIDC?

A cryptographically random value sent with the auth request and embedded in the ID token to prevent replay attacks.

What is the state parameter in OIDC?

A value that links the authorization response to the original request, preventing CSRF attacks.

Should I store tokens in localStorage?

No. Use HTTP-only, Secure, SameSite cookies to prevent XSS-based token theft.

What is token binding in OIDC?

A mechanism that cryptographically binds tokens to a specific client instance, preventing token export and replay.

How do I handle JWKS key rotation?

Cache JWKS with a TTL, and when key validation fails, fetch fresh keys and retry before rejecting.

Mini Project

Build a security audit tool that scans an OIDC implementation for common vulnerabilities: checks for nonce usage, state validation, cookie security flags, HTTPS enforcement, and JWKS rotation. Generate a report with severity levels and remediation steps.

What's Next

  • Explore popular OIDC providers and their features
  • Learn about OIDC testing strategies with mock providers
  • Continue to building a complete OIDC project

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro