Skip to main content
Cloud & AI Hub
Browse
Glossary AI Directory Playgrounds Models Prompts Explainers Strategy Matrix Benchmark Decoder
intermediate 9 mins Read

How OAuth 2.0 Authorization Code Flow Works

A technical walkthrough of the OAuth 2.0 Authorization Code flow with PKCE, JWT access tokens, refresh token lifecycle, and token introspection.

Introduction

OAuth 2.0’s Authorization Code flow is the most secure grant type for server-side applications and native mobile apps that need to access protected APIs on behalf of a user. Unlike the now-deprecated Implicit flow (which returned access tokens directly in URL fragments), the Authorization Code flow separates the authorization step (user grants consent) from the token exchange step (server-to-server code swap), keeping access tokens out of the browser’s URL bar, history, and referrer headers. The code itself is short-lived (typically 60-600 seconds) and single-use, limiting the blast radius if intercepted.

The introduction of PKCE (Proof Key for Code Exchange, RFC 7636) extended the Authorization Code flow to clients that cannot maintain a confidential secret — single-page apps and native mobile apps. PKCE replaces the client_secret with a cryptographically derived challenge-verifier pair generated fresh for every authorization request, preventing authorization code interception attacks where a malicious app on the same device could intercept the redirect URI callback and exchange the code first.

Step-by-Step: OAuth 2.0 Authorization Code Flow with PKCE

flowchart TD
    A["1. Redirect to Authorization Endpoint"]
    B["2. User Authentication and Consent"]
    C["3. Authorization Code Returned via Redirect"]
    D["4. Code Exchange at Token Endpoint"]
    E["5. JWT Access Token Structure"]
    F["6. Refresh Token Lifecycle"]
    G["7. Token Introspection (RFC 7662)"]
    A --> B
    B --> C
    C --> D
    D --> E
    E --> F
    F --> G

Step 1: Redirect to Authorization Endpoint

The client (your application) redirects the user’s browser to the authorization endpoint of the authorization server (e.g., https://auth.example.com/oauth/authorize). The redirect URL includes these critical parameters:

https://auth.example.com/oauth/authorize?
  response_type=code
  &client_id=myapp_client_id
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20profile%20email%20api%3Aread
  &state=xK3mVp9wQ2nR7sT4
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
  • response_type=code: Requests an authorization code (not a token)
  • scope: Space-separated list of requested permissions; the user will see a consent screen describing each
  • state: A random, opaque, unguessable value stored in the client’s session; returned unchanged in the callback to prevent CSRF attacks
  • code_challenge: PKCE — the SHA-256 hash of the code_verifier, Base64url-encoded (no padding): code_challenge = BASE64URL(SHA256(code_verifier))
import secrets, hashlib, base64

# Generate PKCE parameters
code_verifier = secrets.token_urlsafe(64)  # 43-128 chars, URL-safe
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()

print(f"Verifier: {code_verifier}")
print(f"Challenge: {code_challenge}")

The user is presented with the authorization server’s login page (if not already authenticated). After login, the server shows a consent screen listing the requested scopes. The user clicks “Allow”. This step is entirely between the user and the authorization server — your application’s server is not involved. The authorization server records the user’s consent decision.

Step 3: Authorization Code Returned via Redirect

The authorization server redirects the user’s browser back to your redirect_uri with the authorization code:

https://app.example.com/callback?
  code=SplxlOBeZQQYbYS6WxSbIA
  &state=xK3mVp9wQ2nR7sT4

Your server must validate the state parameter against the value stored in the session before proceeding — if they don’t match, abort immediately (CSRF attack). The authorization code is typically valid for 60-600 seconds and can only be used once.

Step 4: Code Exchange at Token Endpoint (Server-to-Server)

Your backend server (not the browser) makes a POST request to the token endpoint, exchanging the code for tokens. This is a direct server-to-server call that the user’s browser never sees:

POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&client_id=myapp_client_id
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk...

The authorization server:

  1. Validates that code exists and hasn’t expired or been used
  2. Validates redirect_uri exactly matches the registered URI
  3. Computes SHA256(code_verifier) and verifies it equals the stored code_challenge
  4. Returns the token response:
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "8xLOxBtZp8",
  "scope": "openid profile email api:read",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Step 5: JWT Access Token Structure

The access_token is typically a JWT (JSON Web Token) with three Base64url-encoded parts separated by dots:

# Decode a JWT access token (never paste real tokens into online tools)
echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImV4cCI6MTcwMDAwMDAwMCwic2NvcGUiOiJhcGk6cmVhZCJ9.SIGNATURE" \
  | cut -d. -f2 \
  | base64 -d 2>/dev/null | python3 -m json.tool

A decoded payload looks like:

{
  "sub": "user_123",
  "aud": "api.example.com",
  "iss": "https://auth.example.com",
  "exp": 1700000000,
  "iat": 1699996400,
  "scope": "api:read",
  "jti": "unique-token-id"
}

Resource servers validate the JWT by verifying its RS256 (or ES256) signature against the authorization server’s public key (fetched from the JWKS endpoint: https://auth.example.com/.well-known/jwks.json).

Step 6: Refresh Token Lifecycle

Access tokens are short-lived (typically 1 hour). The refresh token is a long-lived opaque credential (days to months) that your server stores securely. When the access token expires:

POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=8xLOxBtZp8
&client_id=myapp_client_id

The server responds with a new access token and optionally rotates the refresh token (refresh token rotation is a security best practice — each use invalidates the old token). If a rotated refresh token is used twice, it indicates theft and the authorization server should revoke the entire token family.

Step 7: Token Introspection (RFC 7662)

For opaque tokens (non-JWT), or when resource servers want to validate without implementing JWT verification themselves, token introspection allows a resource server to query the authorization server:

POST /oauth/introspect HTTP/1.1
Host: auth.example.com
Authorization: Basic <resource_server_credentials>
Content-Type: application/x-www-form-urlencoded

token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

{
  "active": true,
  "sub": "user_123",
  "scope": "api:read",
  "exp": 1700000000,
  "client_id": "myapp_client_id"
}

Key Takeaways

  • PKCE eliminates the need for client secrets on public clients: The code_verifier / code_challenge pair provides a cryptographic proof that only the original requester can complete the flow, even without a shared secret.
  • state parameter is your CSRF defense: Always generate a cryptographically random state value, store it in the user’s session, and reject any callback where it doesn’t match exactly.
  • Authorization codes are single-use and short-lived: They should expire within 60-600 seconds and be invalidated immediately upon use — reuse of a code is a signal of a replay attack.
  • JWT access tokens enable stateless validation at the resource server: By verifying the JWT signature against the authorization server’s JWKS public key, resource servers avoid a round-trip to the authorization server on every request.
  • Refresh token rotation prevents indefinite token theft: Each refresh token use should invalidate the previous token; if a previously used refresh token is presented again, revoke the entire token family immediately.

Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.