01

The Problem OAuth 2.0 Solves

Imagine you're building a travel app. You want to let users import their Google Calendar events. The old way: ask users for their Google username and password, store those credentials, and use them to call the Google API on their behalf.

DANGER

Why storing passwords is catastrophic: If your database gets breached, attackers get full access to all your users' Google accounts โ€” not just your app. Users have no way to revoke access without changing their entire password. Google has no way to know if it's really you or an attacker.

OAuth 2.0 solves this with delegated authorization: instead of sharing passwords, users grant your app a limited, revocable permission token. The password never leaves Google.

KEY DISTINCTION

OAuth 2.0 is AUTHORIZATION, not AUTHENTICATION. It answers "what can this app do?" โ€” not "who is this user?" For identity (authentication), you use OpenID Connect (OIDC), which is a thin layer on top of OAuth 2.0. "Login with Google" actually uses both.

The Four Roles

๐Ÿ‘ค

Resource Owner (You)

The human who owns the data. You own your Google Calendar. You decide what apps can access it. Without your explicit approval at the consent screen, nothing happens.

๐Ÿ“ฑ

Client (The App)

The application requesting access to your data. Could be a web app, mobile app, or CLI tool. Registered with the Authorization Server and has a client_id and optionally a client_secret.

๐Ÿ›๏ธ

Authorization Server (Google)

Authenticates the user, shows the consent screen, issues tokens. This is accounts.google.com for Google. It's the only party that ever sees your password.

๐Ÿ—„๏ธ

Resource Server (Google API)

The API that holds your data โ€” calendar.googleapis.com, gmail.googleapis.com, etc. Accepts access tokens and returns data. Validates tokens against the Authorization Server's public keys.

02

The 4 OAuth 2.0 Grant Types

OAuth 2.0 defines four ways to obtain tokens, called grant types. Each is designed for a different client type and security model.

Grant Type Use When Client Secret? Status
Authorization Code Web app with backend server Yes RECOMMENDED
Auth Code + PKCE SPA, mobile app, CLI No RECOMMENDED
Client Credentials Machine-to-machine (no user) Yes USE FOR M2M
Implicit Older SPAs (pre-PKCE) No DEPRECATED
WHY IMPLICIT WAS DEPRECATED

Implicit flow returned the access token directly in the URL fragment (#access_token=ya29...). This leaks the token into browser history, server logs, and the Referer header. PKCE solves the same problem (public client, no secret) without the URL exposure.

03

Authorization Code Flow โ€” Every HTTP Request

This is the main event. A web app with a backend server wants to access the user's Google Calendar. Here's every single HTTP request that happens โ€” nothing hidden.

1

App Redirects User to Authorization Server

User clicks "Sign in with Google". Your backend constructs a URL and sends an HTTP 302 redirect to the browser.

HTTP Request โ€” Step 1
GET https://accounts.google.com/o/oauth2/v2/auth ?response_type=code &client_id=123456789.apps.googleusercontent.com &redirect_uri=https://myapp.com/auth/callback &scope=openid email profile https://www.googleapis.com/auth/calendar.readonly &state=xK9mN2pQrL7wZ3aY โ† random CSRF token &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 โ† PKCE &access_type=offline โ† request refresh token
STATE PARAM

The state value is a random string your server generates and stores in the session. When Google redirects back, you verify it matches. This prevents CSRF attacks where an attacker tricks the user into completing an OAuth flow that the attacker initiates.

2

User Logs In + Sees Consent Screen

Google's server renders its own login page and consent screen. Your app never sees the user's password โ€” it all happens inside Google's domain. The user sees exactly which permissions they're granting.

CONSENT SCREEN

Google shows: "MyApp wants access to: Read your calendar events" with Allow/Deny buttons. The scope parameter controls what's shown. Users can revoke this at any time from their Google Account settings.

3

Google Redirects Back With Authorization Code

After consent, Google's server redirects the browser back to your redirect_uri with a short-lived authorization code. This code is single-use and expires in ~10 minutes.

HTTP Response โ€” Step 3
HTTP/1.1 302 Found Location: https://myapp.com/auth/callback ?code=4/0AVG7fiRnX2kbD9pMm-tzHe9fFRkOqnZ5_example_code &state=xK9mN2pQrL7wZ3aY โ† verify this matches! &scope=email profile openid https://www.googleapis.com/auth/calendar.readonly
WHY A CODE INSTEAD OF TOKEN?

The code travels through the browser (URL bar, browser history, Referer header). It's single-use and expires fast. The real tokens are fetched server-to-server in the next step โ€” never touching the browser. This is the key security advantage over Implicit flow.

4

Backend Exchanges Code for Tokens

Your server (not the browser) makes a direct POST request to Google's token endpoint. This is a server-to-server call โ€” the user's browser is not involved.

HTTP Request โ€” Step 4
POST https://oauth2.googleapis.com/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=4/0AVG7fiRnX2kbD9pMm-tzHe9fFRkOqnZ5_example_code &redirect_uri=https://myapp.com/auth/callback &client_id=123456789.apps.googleusercontent.com &client_secret=GOCSPX-your_client_secret_here โ† only server has this &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk โ† PKCE verifier
5

Google Returns the Tokens

Google validates the code, client credentials, and PKCE verifier, then returns three tokens.

HTTP Response โ€” Step 5
HTTP/1.1 200 OK Content-Type: application/json { "access_token": "ya29.a0AfH6SMBxL2ZjQVT8a...", โ† use for API calls "token_type": "Bearer", "expires_in": 3599, โ† 1 hour "refresh_token": "1//0gLk9mVOz7kX2KgBCT...", โ† long-lived "scope": "openid email profile https://www.googleapis.com/auth/calendar.readonly", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." โ† identity JWT (OIDC) }
6

Use Access Token to Call the API

Every subsequent API call includes the access token in the Authorization header. Google's Resource Server validates the token signature and checks scopes before returning data.

HTTP Request โ€” Step 6
GET https://www.googleapis.com/calendar/v3/calendars/primary/events Authorization: Bearer ya29.a0AfH6SMBxL2ZjQVT8a... Accept: application/json
04

Animated Flow Diagram

Step through the entire OAuth 2.0 Authorization Code flow as an animated sequence diagram. Watch exactly who sends what to whom.

Step 0 / 7 READY
๐Ÿ‘ค USER ๐Ÿ“ฑ YOUR APP ๐Ÿ›๏ธ AUTH SERVER ๐Ÿ—„๏ธ RESOURCE API Click "Login with Google" GET /auth?response_type=code &scope=openid email &state=xyz &code_challenge=... Show Login + Consent Screen User Approves Consent 302 โ†’ /callback?code=4/0AVG... POST /token grant_type=auth_code &client_secret &code_verifier GET /calendar/events Authorization: Bearer ya29.a0... AUTHENTICATED โœ“
Press "step โ†’" to walk through each exchange, or "โ–ถ play all" to watch the full OAuth 2.0 flow.
05

PKCE โ€” Proof Key for Code Exchange

PKCE (pronounced "pixy") extends Authorization Code flow for public clients โ€” apps that can't keep a client_secret confidential. That means: single-page apps, mobile apps, and CLI tools.

THE ATTACK PKCE PREVENTS

On mobile, multiple apps can register the same URL scheme (e.g. myapp://callback). A malicious app could intercept the auth code redirect. Without PKCE, the attacker can use that code to get tokens. With PKCE, the code is worthless without the original code_verifier โ€” which never left the legitimate app.

How PKCE Works (3 steps)

GEN

Generate code_verifier + code_challenge

// Before redirecting to Auth Server: code_verifier = random_string(43-128 chars, URL-safe) // e.g.: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" code_challenge = base64url(SHA256(code_verifier)) // e.g.: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" // Store code_verifier locally (never sent until token exchange) // Send code_challenge in the authorization URL
REQ

Include code_challenge in authorization request

Add &code_challenge=E9Melhoa...&code_challenge_method=S256 to the authorization URL. Auth Server stores this hash.

VFY

Send code_verifier when exchanging code for tokens

Add &code_verifier=dBjftJeZ... to the token exchange POST. Auth Server computes SHA256 of this verifier and checks it matches the stored challenge. If an attacker intercepted the code, they don't have the verifier โ€” exchange fails.

06

JWT Deep Dive

JSON Web Tokens (JWTs) are the most common format for OAuth 2.0 access tokens and OIDC id_tokens. They're self-contained โ€” the Resource Server can validate them without calling the Auth Server.

Structure: Three Base64url-Encoded Parts

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiMTIzLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiZXhwIjoxNzUwMDAwMDAwLCJpYXQiOjE3NDk5OTY0MDAsImVtYWlsIjoiYWxpQGV4YW1wbGUuY29tIiwibmFtZSI6IkFsaSBBbndhciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
โ–  HEADER โ–  PAYLOAD โ–  SIGNATURE
Header (blue)
{ "alg": "RS256", โ† RSA + SHA256 "typ": "JWT", "kid": "abc123" โ† key ID for rotation }
Payload / Claims (purple)
{ "iss": "https://accounts.google.com", "sub": "1234567890", โ† user ID "aud": "123.apps.googleusercontent.com", "exp": 1750000000, โ† expiry (Unix ts) "iat": 1749996400, โ† issued at "email": "ali@example.com", "name": "Ali Anwar" }
JWT IS NOT ENCRYPTED

The payload is base64url encoded, not encrypted. Anyone can decode it and read the claims. Never put sensitive data in a JWT payload โ€” passwords, credit card numbers, PII that shouldn't be shared. The signature only prevents tampering, not reading.

Signature Verification (RS256)

RS256 Signature Verification
// Authorization Server (Google) signs with PRIVATE key: signature = RSA_Sign( privateKey, SHA256(base64url(header) + "." + base64url(payload)) ) // Resource Server validates with PUBLIC key (fetched from JWKS endpoint): isValid = RSA_Verify( publicKey, โ† from https://accounts.google.com/.well-known/openid-configuration signature, SHA256(base64url(header) + "." + base64url(payload)) ) // Also check: exp > now, aud == your client_id, iss == expected issuer
07

Interactive JWT Decoder

Paste any JWT below to decode and inspect it in real time. The decoder runs entirely in your browser โ€” nothing is sent to a server.

๐Ÿ” JWT DECODER client-side only ยท never leaves browser
HEADER
โ€”
PAYLOAD
โ€”
08

Access Tokens vs Refresh Tokens

Access Token
  • Short-lived: 1 hour (3600s)
  • JWT format (self-contained)
  • Sent in Authorization: Bearer header on every API call
  • Resource Server validates locally (no Auth Server call)
  • Stateless: revocation takes up to 1 hour to take effect
Refresh Token
  • Long-lived: days to months
  • Opaque string (not a JWT)
  • Stored securely server-side (or HttpOnly cookie)
  • Only sent to Auth Server's /token endpoint
  • Token rotation: each use invalidates old token

Token Refresh Flow

Refreshing an Expired Access Token
POST https://oauth2.googleapis.com/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &refresh_token=1//0gLk9mVOz7kX2KgBCT... &client_id=123456789.apps.googleusercontent.com &client_secret=GOCSPX-your_secret // Response: { "access_token": "ya29.a0NEW_TOKEN...", "expires_in": 3599, "refresh_token": "1//0gNEW_REFRESH..." โ† old one is now revoked (rotation) }

Where to Store Tokens

StorageXSS RiskCSRF RiskRecommendation
HttpOnly CookieNone (JS can't read)Yes (need SameSite=Strict)BEST
localStorageHigh (XSS reads it)NoneAVOID
sessionStorageHigh (XSS reads it)NoneMEDIUM
In-memory (JS variable)Medium (loses on refresh)NoneACCEPTABLE for SPAs
09

OpenID Connect (OIDC)

OAuth 2.0 handles authorization (what can this app do). But "Login with Google" also needs authentication (who is this user). That's OpenID Connect โ€” a thin identity layer built on top of OAuth 2.0.

OAUTH 2.0

Answers: "What can this app access?"
Returns: access_token (for API calls)
Example: "This app can read your calendar."

OIDC

Answers: "Who is this user?"
Returns: id_token (identity JWT)
Example: "This user is ali@example.com, Google user ID 112345."

To trigger OIDC, include openid in the scope. The token response then includes an id_token โ€” a JWT containing identity claims (sub, email, name, etc.).

LOGIN WITH GOOGLE = OAUTH 2.0 + OIDC

Scope: openid email profile
Returns: access_token (to call Google APIs) + id_token (to know who logged in)
Your backend: verifies id_token signature, extracts sub (stable user ID) + email, creates/updates your user record.

10

Common Security Mistakes

CSRF

Not Validating the state Parameter

If you don't check that state matches what you sent, an attacker can craft an authorization URL, trick a victim into clicking it, and then inject their own callback URL with a legitimate code. Always validate state as a CSRF token.

REDIR

Not Validating redirect_uri Exactly

If the Auth Server does prefix matching (e.g. allows any https://myapp.com/...), an attacker can register a subdomain and steal codes. Auth Servers must do exact matching of pre-registered redirect URIs. Register only specific full URIs, not wildcards.

AUD

Not Checking the aud Claim

A JWT issued for Client A has "aud": "client_a_id". If Client B accepts tokens without verifying aud, an attacker with a token from Client A can use it at Client B. Always verify the aud claim equals your own client_id.

PKCE

Not Using PKCE for Public Clients

SPAs and mobile apps that use Authorization Code flow without PKCE are vulnerable to authorization code interception. Always use PKCE for any client that can't keep a secret. Some Auth Servers now require it for all clients.

LOG

Tokens Leaking into Logs

If you log all HTTP requests (common in debugging), access tokens in Authorization headers end up in logs. Sanitize or redact Authorization headers before logging. Never put tokens in URL query parameters in production.

11

Client Credentials Flow (Machine-to-Machine)

When a backend service needs to call another backend service โ€” no user involved โ€” use Client Credentials flow. A cron job calling an internal API, a microservice calling another microservice, a CI/CD pipeline uploading artifacts.

Client Credentials Token Request
POST https://auth.example.com/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) grant_type=client_credentials &scope=api:read api:write // Response: { "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 3600 } // No refresh_token โ€” just request a new access_token when it expires
12

Cheat Sheet

Grant Type Comparison

FlowClient TypeSecret?User Involved?ReturnsUse Case
Auth Code Web (backend) Yes Yes access + refresh + id Traditional web apps
Auth Code + PKCE SPA / Mobile No Yes access + refresh + id React, React Native, iOS
Client Credentials Backend service Yes No access only Microservices, cron jobs
Implicit (deprecated) SPA (old) No Yes access only (in URL) Don't use

Important JWT Claims

ClaimFull NameTypeWhat to Check
issIssuerstring (URL)Must match expected Auth Server URL
subSubjectstring (user ID)Stable unique user identifier
audAudiencestring or arrayMust include your client_id
expExpirationUnix timestampMust be in the future
iatIssued AtUnix timestampShould be recent (clock skew)
jtiJWT IDstringCheck for token replay attacks
Security Checklist
  • โœ… Validate state (CSRF)
  • โœ… Exact redirect_uri match
  • โœ… Verify aud claim
  • โœ… Check exp claim
  • โœ… Use PKCE for public clients
  • โœ… Store tokens in HttpOnly cookie
  • โœ… Rotate refresh tokens
  • โœ… Use HTTPS everywhere
JWKS Endpoint

Auth Servers publish public keys at:

# Google OIDC config: https://accounts.google.com/.well-known/openid-configuration # JWKS endpoint: https://www.googleapis.com/oauth2/v3/certs
Useful Headers
# Revoke a token: POST /revoke token=ACCESS_OR_REFRESH # Introspect a token: POST /introspect token=ACCESS_TOKEN # โ†’ {"active":true,...}
END OF HOW-03