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 TypeUse CaseSecurity LevelRequires Client Secret?
Authorization Code + PKCESingle-page apps, mobile apps, all modern web appsHighestNo (PKCE replaces the secret)
Authorization Code (classic)Server-rendered web apps (backend can keep a secret)HighYes
Client CredentialsMachine-to-machine, service accounts, API integrationsMediumYes
Device CodeTV apps, CLI tools, IoT devices (input-constrained)MediumNo
Refresh TokenRenew access tokens without re-authenticationN/AYes (usually)
Implicit (DEPRECATED)Do NOT use — insecure, tokens in URL fragmentNoneDo 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

PracticeWhyImplementation
Validate the state parameterPrevents CSRF attacks on the callback endpointCompare received state with session-stored state
Use PKCE for ALL clientsPrevents authorization code interceptionEven for confidential clients — it is free security
Validate ID tokensPrevents token injection and replay attacksCheck signature, issuer, audience, expiration, nonce
Store tokens server-sideAccess tokens in browser local storage are XSS-vulnerableHttpOnly, Secure, SameSite cookies for session ID
Use short-lived access tokensLimits damage if a token is leaked5-15 minutes; use refresh tokens for renewal
Implement token rotationEach refresh token use returns a new refresh tokenInvalidate the old refresh token after rotation

OAuth Providers: Self-Hosted vs Managed

ProviderTypeBest ForPricing
Auth0ManagedEnterprise, comprehensive identity managementFree (7,500 MAU), $25/mo (1K MAU)
ClerkManagedReact/Next.js apps, best developer experienceFree (10K MAU), $25/mo (1K MAU)
Lucia AuthLibrary (self-hosted)Full control, TypeScript-nativeFree (MIT)
Supabase AuthManaged + Self-hostedApps already using SupabaseFree (50K MAU), $25/mo (100K MAU)
NextAuth.js (Auth.js)Library (self-hosted)Next.js apps, OAuth provider agnosticFree (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.