7 min read

Q58 — How does OAuth 2.0 work — why does it exist instead of just passing passwords?


What problem does OAuth solve?

Imagine you want to use an app — say, a calendar tool — that needs to read your Google Calendar. The naive solution: give the calendar tool your Google username and password. It logs in as you, reads your calendar.

This is deeply broken:

  • You've given a third-party app full credentials to your Google account — not just calendar access, but Gmail, Drive, Photos, and everything else
  • You cannot selectively revoke this app's access without changing your password (which revokes all apps)
  • The third-party app now stores your credentials somewhere — creating another place your password can be compromised
  • Google has no way to know the calendar app is acting on your behalf vs an attacker who stole the credentials

The core problem: you want to grant an application limited, scoped access to a resource you own (your Google Calendar), without sharing your password with that application.

OAuth 2.0 is the industry-standard protocol for exactly this: delegated authorization without credential sharing.


The four roles in OAuth 2.0

Before the flow, the vocabulary:

Resource Owner: the user — the person who owns the protected resource (your Google Calendar).

Resource Server: the server that hosts the protected resource and enforces access control (Google Calendar API).

Authorization Server: the server that authenticates the user and issues tokens (Google's OAuth server at accounts.google.com). In practice, the authorization server and resource server are often run by the same company (Google), but they're conceptually separate.

Client: the application that wants access to the resource (the calendar tool). The client can be a web app, a mobile app, or a server-side application.


The Authorization Code flow — the most important flow

The authorization code flow is the standard flow for web applications where there's a server-side component. It's the flow you're using when you click "Sign in with Google."

Step 1 — The client redirects the user to the authorization server:

code
GET https://accounts.google.com/o/oauth2/auth
  ?client_id=calendar-app-id
  &redirect_uri=https://calendartool.com/callback
  &scope=https://www.googleapis.com/auth/calendar.readonly
  &response_type=code
  &state=random-csrf-token

Key parameters: scope (what access is being requested — read-only calendar, not full account), redirect_uri (where to send the user after authorization), state (a random value to prevent CSRF).

Step 2 — The user authenticates and consents:

The user sees Google's login page (or is already logged in). Then they see a consent screen: "Calendar Tool wants to read your Google Calendar. Allow?" The user clicks Allow.

This is the critical moment: the user authenticated with Google directly — the calendar tool never saw the password. The user explicitly consented to exactly the scope requested.

Step 3 — The authorization server redirects back with a code:

code
GET https://calendartool.com/callback
  ?code=4/abc123...
  &state=random-csrf-token

The code is a short-lived (60 seconds), single-use authorization code. It's not the token — it's proof that the user authorized this client for this scope.

Step 4 — The client exchanges the code for tokens (server-to-server):

code
POST https://accounts.google.com/o/oauth2/token
  client_id=calendar-app-id
  client_secret=secret-only-server-knows
  code=4/abc123...
  redirect_uri=https://calendartool.com/callback
  grant_type=authorization_code

This request is server-to-server, not browser-based. It includes the client_secret — a credential the authorization server issued to the calendar tool when it registered. The calendar tool proves its own identity here.

Step 5 — The authorization server returns tokens:

json
{
  "access_token": "ya29.xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "1//yyy",
  "scope": "https://www.googleapis.com/auth/calendar.readonly"
}

The access token is short-lived (1 hour). The refresh token is long-lived and can be used to get new access tokens without user interaction.

Step 6 — The client uses the access token:

code
GET https://www.googleapis.com/calendar/v3/calendars
  Authorization: Bearer ya29.xxx

The Google Calendar API verifies the access token with the authorization server, confirms it has the calendar.readonly scope, and returns the calendars. Done.


Why the two-step code → token exchange?

Why not return the access token directly in the redirect (step 3)? Because the redirect goes through the browser URL — which appears in browser history, server logs, and referrer headers. A token in the URL can leak.

The authorization code is short-lived, single-use, and useless without the client_secret. By exchanging it server-to-server (step 4), the final access token never appears in a URL. It's only in the server's memory and in the token response body, which is delivered over HTTPS directly to the server.


The PKCE extension — for public clients

Mobile apps and single-page applications can't safely store a client_secret (the code is on the user's device, which can be inspected). They're "public clients." Without a client secret, step 4 has no server-to-server authentication — anyone who intercepts the code could exchange it for a token.

PKCE (Proof Key for Code Exchange, pronounced "pixie") fixes this:

  1. The client generates a random code_verifier and computes code_challenge = SHA256(code_verifier)
  2. Step 1: include code_challenge in the authorization request
  3. Step 4: include code_verifier in the token exchange; the authorization server verifies SHA256(code_verifier) == code_challenge

An interceptor who captures the authorization code doesn't have the code_verifier, so they can't exchange the code. PKCE is mandatory for mobile and SPA clients.


OAuth 2.0 vs OpenID Connect — authorization vs authentication

OAuth 2.0 is an authorization protocol — it grants access to resources. The access token says "the bearer of this token may read this user's calendar." It says nothing about who the bearer is or whether they're authenticated.

OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that adds authentication. It returns an additional ID token (a JWT) alongside the access token. The ID token contains: who the user is (sub — user identifier), when they authenticated, what email they have. This is the "sign in with Google" use case — you're not requesting resource access, you're asking Google to authenticate the user and tell you who they are.

The distinction matters: OAuth = "can this app access this resource on behalf of this user?" OIDC = "who is this user, and can Google vouch for them?"

In practice, "Sign in with Google" uses OIDC; "Connect your Google Calendar" uses OAuth 2.0 (often with OIDC mixed in for the user identity).


Why OAuth beats every alternative

vs. sharing passwords: no password exposure to the third party, no full account access, revocable at any time without changing password.

vs. API keys: OAuth tokens are scoped to specific permissions and expire. API keys are typically long-lived and all-or-nothing. OAuth supports per-token revocation and per-scope grants.

vs. custom SSO: OAuth 2.0 is a standard. Every major platform (Google, GitHub, Facebook, Stripe) implements it. Building on OAuth means any compliant client can integrate with any compliant server without custom negotiation.


The mental model

code
Problem: App needs access to your data on another service
  Naive: give app your password → full access, no revocation, credential exposure
  OAuth: delegated, scoped, revocable access without password sharing

Four roles:
  Resource Owner: you (own the data)
  Authorization Server: issues tokens (Google login)
  Resource Server: protects data (Google Calendar API)
  Client: app requesting access (Calendar Tool)

Authorization Code Flow (web apps):
  1. Client → redirect user to AuthServer (with scope, redirect_uri, state)
  2. User authenticates with AuthServer directly (never touches client)
  3. User consents to scoped access
  4. AuthServer → redirect back to client with short-lived code
  5. Client → server-to-server: exchange code + client_secret → tokens
     (server-to-server keeps token out of browser URL/history)
  6. Client calls ResourceServer with access_token in Authorization header

Tokens:
  Access token: short-lived (1hr), bearer token, scoped to authorized resources
  Refresh token: long-lived, used to get new access tokens silently
  ID token (OIDC): JWT with user identity (who they are, not what they can access)

PKCE (for mobile/SPA — no client_secret):
  code_verifier (random) → code_challenge (hash) sent with auth request
  code_verifier sent with token exchange → server verifies match
  Interceptor can't exchange code without verifier

OAuth vs OIDC:
  OAuth = authorization ("can this app access this resource?")
  OIDC = authentication ("who is this user?") → adds ID token (JWT)
  "Sign in with Google" = OIDC; "Connect Google Calendar" = OAuth 2.0

Why OAuth wins:
  No password exposure → third party never sees credentials
  Scoped → calendar access, not full account
  Revocable → user revokes per-app, no password change
  Short-lived tokens → limited blast radius on compromise
  Standard → every major platform implements it

OAuth 2.0 exists because delegation is fundamentally different from authentication — you're not proving who you are to the third-party app, you're having a trusted authority (Google) certify what the third-party app is allowed to do on your behalf. The password-sharing model conflates these two problems; OAuth separates them cleanly.