Zoom-in: OAuth 2.0

"Sign in with Google" — three words, one click. Behind that is a mechanism where the password never leaves Google, the app never sees the user's credentials, and the user can revoke access at any time without changing their password.
Zoom in.
Layer 1 — The delegation problem
The old approach: to let App A read a user's Google email, the user must hand over their username and password to App A.
graph LR
U["👤 User"] -->|"username + password"| A["📱 App A"]
A -->|"login as user"| G["🔑 Google"]
style U fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style A fill:#1a3a2a,stroke:#22c55e,color:#86efac
style G fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
The problem: App A has full access to the Google account — no scope limits, no way to revoke independently, and if App A leaks data then the user's Google password is compromised too.
OAuth 2.0 solves this with a different model: instead of handing over the house key, issue a token with a limited lifespan and scope.
Layer 2 — Authorization Code flow
OAuth 2.0 splits the process into two steps: get a code → exchange code for token.
sequenceDiagram
participant U as 👤 User
participant A as 📱 App (Client)
participant G as 🔑 Google Auth Server
participant R as 📦 Resource Server (Gmail API)
U->>A: "Sign in with Google"
A->>G: Redirect → /authorize?client_id=...&scope=email&redirect_uri=...&state=xyz
G-->>U: Show consent screen
U->>G: Grant permission
G-->>A: Redirect to redirect_uri?code=AUTH_CODE&state=xyz
A->>G: POST /token (code=AUTH_CODE + client_secret)
G-->>A: access_token + refresh_token
A->>R: GET /gmail/messages (Authorization: Bearer access_token)
R-->>A: Email data
The authorization code lives for only a few seconds and is single-use — it travels through the browser URL, but it's not the real token. The actual token (access_token) is exchanged server-to-server using client_secret, never through the browser.
The state parameter is a random string generated by the app — used to prevent CSRF: if the response returns a different state than what was sent, the flow is aborted.
Layer 3 — Access token vs Refresh token
Two tokens, two distinct purposes.
graph LR
A["📱 App"] -->|"access_token (15 min)"| R["📦 Resource Server"]
A -->|"refresh_token (30 days)"| G["🔑 Auth Server"]
G -->|"new access_token"| A
style A fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style R fill:#1a3a2a,stroke:#22c55e,color:#86efac
style G fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
| Access token | Refresh token | |
|---|---|---|
| Sent to | Resource server (every request) | Auth server (when access token expires) |
| Lifespan | 15 min – 1 hour | Days to months |
| If leaked | Dangerous for a short window | More dangerous, but revocable |
Splitting into two tokens reduces risk: the short-lived access token limits the attack window if leaked. The refresh token only goes to the auth server — far less exposure surface.
client_secret — which is only safe in a server-side app. Mobile apps and SPAs have nowhere to store a secret safely.
Layer 4 — PKCE: protecting public clients
Mobile apps and browser SPAs are public clients — no server, nowhere to keep a client_secret. PKCE (Proof Key for Code Exchange) replaces the client secret with a temporary key pair generated per request.
sequenceDiagram
participant A as 📱 Mobile App
participant G as 🔑 Auth Server
Note over A: Generate code_verifier (random, 43-128 chars)
Note over A: code_challenge = SHA256(code_verifier)
A->>G: /authorize?...&code_challenge=...&code_challenge_method=S256
G-->>A: code=AUTH_CODE
A->>G: /token (code=AUTH_CODE + code_verifier)
Note over G: SHA256(code_verifier) == code_challenge? ✓
G-->>A: access_token + refresh_token
The code_verifier only lives in the app's memory and never leaves the app until the token exchange step. Even if AUTH_CODE is intercepted, an attacker without the code_verifier cannot exchange it for a token.
Full picture
sequenceDiagram
participant U as 👤 User
participant A as 📱 App
participant G as 🔑 Auth Server
participant R as 📦 Resource Server
Note over A: Generate state + PKCE (if public client)
A->>G: /authorize (scope, redirect_uri, state, code_challenge)
G-->>U: Consent screen
U->>G: Grant access
G-->>A: code + state
Note over A: Validate state
A->>G: /token (code + client_secret or code_verifier)
G-->>A: access_token (15min) + refresh_token (30 days)
loop Per request
A->>R: API call (Bearer access_token)
R-->>A: Data
end
Note over A: access_token expires
A->>G: /token (refresh_token)
G-->>A: new access_token
Takeaway
OAuth 2.0 separates three concerns: who the user is, who is granted permission, and what that permission covers. The password never leaves the auth server. Tokens carry scoped, time-limited access. The refresh token maintains the session without prompting the user again.
Two implementation details that often get skipped: validate state to prevent CSRF, and use PKCE for all public clients — even when client_secret is supported, because PKCE adds a protection layer at no real cost.
This post was assisted by Amy 🌸 - AI Assistant. Content has been reviewed by the author.
Related Posts
Zoom-in: JWT
Log in, call an API, server lets you through. Inside that token is a digital signature — not encryption, and that distinction matters.
Zoom-in: Asymmetric Encryption
HTTPS is secure because it's encrypted. But who encrypted it, who decrypts it, and why no one can impersonate the server — that's the real question.
Zoom-in: TCP
Every HTTP request runs on TCP — but before the first byte of real data crosses the wire, three packets are exchanged carrying no data at all. TCP solves the problem the Internet doesn't.