# OAuth Mistakes in Production

## This post is a checklist disguised as war stories

Every item below appears in real audits. OAuth did not fail — configuration and application code did.

* * *

## 1\. Redirect URI laxity

**Mistake:** Register `https://app.example.com/*` or use open redirectors.

**Why it hurts:** Authorization codes go to attacker-controlled pages.

**Fix:**

*   Exact match only (scheme, host, port, path)
    
*   Separate clients for dev/stage/prod
    
*   No wildcard redirect URIs in production
    

![](https://cdn.hashnode.com/uploads/covers/69722030e94f080c107ed87f/b71f63dc-ceb8-4ad8-a363-4d266f9acffa.png align="center")

* * *

## 2\. Missing or weak `state`

**Mistake:** Omit `state` or use a static value.

**Why it hurts:** Login CSRF — victim links attacker's account to victim's session.

**Fix:** Cryptographically random `state`, bound to user agent session, verified on callback.

* * *

## 3\. PKCE not enforced

**Mistake:** Public client without PKCE; or `code_challenge_method=plain`.

**Why it hurts:** Stolen authorization codes become tokens.

**Fix:** S256 PKCE for all clients; OAuth 2.1 makes this mandatory.

* * *

## 4\. Tokens in the wrong place

| Token | Never | Prefer |
| --- | --- | --- |
| Refresh token | `localStorage`, mobile logs | Server-side store, httpOnly cookie |
| Access token | URL query strings | `Authorization` header |
| ID token | Sent to third-party APIs | Client login only |

**Mistake:** SPA stores refresh token in `localStorage` "because it's easier."

**Why it hurts:** Any XSS exfiltrates long-lived credentials.

* * *

## 5\. Trusting the ID token at the API layer

**Mistake:** Resource server accepts `id_token` as bearer token.

**Why it hurts:** ID token is for the **client**, not your orders API. Wrong audience.

**Fix:** APIs require **access tokens** with correct `aud` (or introspection).

* * *

## 6\. JWT validation shortcuts

**Mistake:** Decode JWT, read claims, skip signature verification "in dev."

**Why it hurts:** `alg: none` and key confusion attacks.

**Fix:** Always verify:

*   Signature (JWKS from `iss`)
    
*   `exp` / `nbf`
    
*   `iss` matches expected realm
    
*   `aud` contains your API identifier
    
*   Scope for the operation
    

```java
// Pseudocode — never hand-roll in production without a library
jwt.verifySignature(jwksUrl)
   .requireIssuer("https://auth.example.com/realms/myrealm")
   .requireAudience("orders-api");
```

* * *

## 7\. Confusing authentication with authorization

**Mistake:** "User has a valid token" → full admin access.

**Fix:** Map IdP identity to **your** roles. Check permissions per resource.

* * *

## 8\. One realm to rule them all (badly)

**Mistake:** Dev and prod share a realm; test users in production; admin console on public internet without IP restrict.

**Fix:** Environment isolation, least-privilege admin, MFA on IdP admin accounts.

* * *

## 9\. Ignoring logout and session drift

**Mistake:** Clear only your app cookie; IdP SSO cookie remains.

**Fix:** OIDC end-session endpoint; short access token TTL; refresh rotation.

* * *

## 10\. Logging secrets

**Mistake:** Log full token responses, `client_secret`, `code_verifier`.

**Fix:** Structured logs with token **prefix** only; never log secrets.

* * *

## Production launch checklist

### Authorization server (Keycloak / Okta / Auth0)

*   \[ \] Production realm / tenant separate from dev
    
*   \[ \] Redirect URIs exact match per environment
    
*   \[ \] Client auth method appropriate (secret for confidential, PKCE for public)
    
*   \[ \] Scopes are minimal (no `offline_access` unless needed)
    
*   \[ \] MFA policy defined
    
*   \[ \] Admin access restricted and audited
    
*   \[ \] TLS everywhere; HSTS on auth domain
    

### Application

*   \[ \] `state` validated on every callback
    
*   \[ \] PKCE S256 on all authorization requests
    
*   \[ \] Refresh tokens only server-side (or BFF pattern)
    
*   \[ \] Session cookie: `Secure`, `HttpOnly`, `SameSite=Lax` or `Strict`
    
*   \[ \] CSRF protection on state-changing endpoints
    
*   \[ \] Error pages do not leak codes or tokens in HTML
    

### APIs

*   \[ \] Access token validation with full claim checks
    
*   \[ \] Authorization separate from token presence
    
*   \[ \] Rate limiting and abuse detection
    
*   \[ \] Clock skew tolerance configured (~60s)
    

### Operations

*   \[ \] JWKS caching with key rotation support
    
*   \[ \] Runbook: IdP outage, key rollover, mass revocation
    
*   \[ \] Alerts on spike in `invalid_grant`, `invalid_client`
    

* * *

## Go deeper

### Token binding and DPoP

For high-assurance APIs, consider **Demonstrating Proof-of-Possession (DPoP)** — binds tokens to a client key pair. Supported in some stacks; not universal yet.

### Security headers for SPAs

Content-Security-Policy, `frame-ancestors`, no inline scripts — reduces XSS risk that makes token theft possible.

### Penetration test focus areas

Ask testers to target: redirect manipulation, `state` bypass, ID token misuse, refresh token replay, and scope escalation.

* * *

## What you should know after this post

*   \[ \] You have a launch checklist copied into your project wiki
    
*   \[ \] You know the top three browser-side mistakes (redirect, `state`, storage)
    
*   \[ \] You validate JWTs completely, not cosmetically
    

* * *
