OAuth 2.0 and OpenID Connect (OIDC) are the foundation of modern authentication — every "Sign in with Google/GitHub/Apple" button uses them. But implementing OAuth 2.0 correctly is notoriously tricky: the spec is 80+ pages, and getting it wrong means security vulnerabilities. This guide walks through a complete implementation, from the authorization code flow to PKCE, token storage, and session management.
OAuth 2.0 Grant Types: When to Use Each
| Grant Type | Use Case | Security Level | Requires Client Secret? |
| Authorization Code + PKCE | Single-page apps, mobile apps, all modern web apps | Highest | No (PKCE replaces the secret) |
| Authorization Code (classic) | Server-rendered web apps (backend can keep a secret) | High | Yes |
| Client Credentials | Machine-to-machine, service accounts, API integrations | Medium | Yes |
| Device Code | TV apps, CLI tools, IoT devices (input-constrained) | Medium | No |
| Refresh Token | Renew access tokens without re-authentication | N/A | Yes (usually) |
| Implicit (DEPRECATED) | Do NOT use — insecure, tokens in URL fragment | None | Do NOT use |
The Authorization Code + PKCE Flow (Step by Step)
# Complete OAuth 2.0 + PKCE Flow
# Step 1: Generate PKCE code verifier and challenge
import hashlib, base64, os, secrets
def generate_pkce_pair():
# Code verifier: 43-128 random characters
code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode()
# Code challenge: SHA256 hash of verifier, base64url encoded
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
return code_verifier, code_challenge
code_verifier, code_challenge = generate_pkce_pair()
# Step 2: Redirect user to authorization endpoint
state = secrets.token_urlsafe(32) # CSRF protection
auth_url = (
f"https://provider.com/authorize"
f"?response_type=code"
f"&client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&code_challenge={code_challenge}"
f"&code_challenge_method=S256"
f"&scope=openid+profile+email"
f"&state={state}"
)
# Store state + code_verifier in session; redirect user to auth_url
# Step 3: User authenticates → provider redirects to your callback with ?code=xxx&state=yyy
# Verify state matches (prevents CSRF)
# Step 4: Exchange authorization code for tokens
token_response = requests.post("https://provider.com/token", data={
"grant_type": "authorization_code",
"code": received_code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"code_verifier": code_verifier, # PKCE: proves we initiated the flow
})
tokens = token_response.json()
# tokens.access_token, tokens.id_token, tokens.refresh_token
Token Security Best Practices
| Practice | Why | Implementation |
| Validate the state parameter | Prevents CSRF attacks on the callback endpoint | Compare received state with session-stored state |
| Use PKCE for ALL clients | Prevents authorization code interception | Even for confidential clients — it is free security |
| Validate ID tokens | Prevents token injection and replay attacks | Check signature, issuer, audience, expiration, nonce |
| Store tokens server-side | Access tokens in browser local storage are XSS-vulnerable | HttpOnly, Secure, SameSite cookies for session ID |
| Use short-lived access tokens | Limits damage if a token is leaked | 5-15 minutes; use refresh tokens for renewal |
| Implement token rotation | Each refresh token use returns a new refresh token | Invalidate the old refresh token after rotation |
OAuth Providers: Self-Hosted vs Managed
| Provider | Type | Best For | Pricing |
| Auth0 | Managed | Enterprise, comprehensive identity management | Free (7,500 MAU), $25/mo (1K MAU) |
| Clerk | Managed | React/Next.js apps, best developer experience | Free (10K MAU), $25/mo (1K MAU) |
| Lucia Auth | Library (self-hosted) | Full control, TypeScript-native | Free (MIT) |
| Supabase Auth | Managed + Self-hosted | Apps already using Supabase | Free (50K MAU), $25/mo (100K MAU) |
| NextAuth.js (Auth.js) | Library (self-hosted) | Next.js apps, OAuth provider agnostic | Free (MIT) |
Bottom line: Use a library or managed service for OAuth 2.0 — never implement the protocol from scratch unless you are building an auth provider. The spec is complex and the security consequences of getting it wrong are severe. For most projects, Clerk or Supabase Auth provides the best balance of security, developer experience, and cost. When building your own, always use Authorization Code + PKCE flow — the implicit flow is deprecated and unsafe. See also: Authentication Best Practices and Clerk vs Auth0 vs Lucia.