OAuth 2.0
Delegated authorisation - letting a third-party app act on a user's behalf without sharing their password
What OAuth 2.0 is - and isn't
OAuth 2.0 is an authorisation framework. It answers: can this application access this resource on behalf of this user? It does not answer: who is the user? That is the job of OIDC, which is a layer on top of OAuth 2.0.
The classic example: "Sign in with Google" to a third-party app. You don't give the app your Google password. Instead, Google asks you to confirm what the app is allowed to do (read your profile, access your calendar, etc.) and issues the app a token scoped to exactly those permissions. The app never sees your credentials.
The analogy
A hotel key card. The hotel (authorisation server) issues you a card (token) that opens your room (scoped resource) for a set period. You give the card to a friend to grab your luggage - they can open your room but not check into a new one or access the hotel's systems. The card expires; it can be revoked without changing your locks.
The four roles
- Resource Owner - the user who owns the data and is granting access.
- Client - your application requesting access on the user's behalf.
- Authorisation Server - issues tokens after the user consents. Google's auth server, GitHub's auth server, Auth0, etc.
- Resource Server - the API holding the user's data. Validates the token and serves the resource. Often the same organisation as the authorisation server but a separate service.
Tokens
- Access token- the credential used to call the resource server. Short-lived (typically 1 hour). Opaque to the client - it's just a string; the client doesn't decode it.
- Refresh token - long-lived credential used to get a new access token when the current one expires, without requiring the user to re-authenticate. Stored securely server-side; never sent to the browser.
- Authorisation code - short-lived (seconds to minutes), single-use code exchanged for tokens. Never used directly to access resources.
Scopes
Scopes define what the access token is permitted to do. They're strings agreed upon between the authorisation server and resource server - OAuth doesn't standardise their format, just the mechanism.
# Requesting read-only access to a user's GitHub repos scope=repo:read user:email # Google - requesting profile and calendar read access scope=openid email profile https://www.googleapis.com/auth/calendar.readonly
The user sees the requested scopes on the consent screen and can decline. Your application should request the minimum scopes it actually needs - asking for more than necessary erodes trust and gets consent rejected.
The Authorization Code flow
The standard flow for server-side applications. The key insight is that tokens are never exposed to the browser - the browser only ever sees a short-lived code.
- User clicks "Connect with GitHub". Your server redirects to GitHub's authorisation endpoint with your
client_id, requestedscope, aredirect_uri, and a randomstatevalue. - GitHub authenticates the user (if not already) and shows a consent screen.
- User approves. GitHub redirects to your
redirect_uriwith a short-livedcodeand thestateyou sent. - Your server verifies the
statematches, then exchanges thecodefor tokens by calling GitHub's token endpoint - sendingcode,client_id, andclient_secret. This is a server-to-server call; the browser is not involved. - GitHub returns an
access_token(and optionally arefresh_token). - Your server stores the tokens and uses the access token to call GitHub's API on the user's behalf.
# Step 1 - redirect to authorisation endpoint https://github.com/login/oauth/authorize ?client_id=abc123 &redirect_uri=https://yourapp.com/oauth/callback &scope=repo:read user:email &state=random-csrf-token &response_type=code # Step 4 - server exchanges code for tokens (POST, never in browser) POST https://github.com/login/oauth/access_token client_id=abc123 client_secret=supersecret code=the-code-from-step-3 redirect_uri=https://yourapp.com/oauth/callback
PKCE - for public clients
The Authorization Code flow above requires a client_secret- a credential that only the server knows. SPAs and mobile apps can't keep secrets (the code is shipped to the user), so they can't use client_secret.
PKCE (Proof Key for Code Exchange, pronounced "pixy") solves this. Before the redirect, the client generates a random code_verifier, hashes it to produce a code_challenge, and sends the challenge with the authorisation request. When exchanging the code for tokens, the client sends the original code_verifier. The auth server hashes it and checks it matches the challenge - proving it's the same client that started the flow, without needing a secret.
# Client generates before redirect: code_verifier = random 43-128 char string (kept secret, in memory) code_challenge = BASE64URL(SHA256(code_verifier)) # Sent in authorisation request: &code_challenge=abc... &code_challenge_method=S256 # Sent in token exchange (instead of client_secret): code_verifier=the-original-random-string
PKCE is now recommended for all OAuth clients - including server-side apps - not just public clients. It adds a layer of protection even when a client_secret is also used.
Client Credentials flow - machine to machine
When there's no user involved - a background job, a microservice calling another service, a CI pipeline - use the Client Credentials flow. The client authenticates directly with the authorisation server using its own credentials, not on behalf of anyone.
POST https://auth.example.com/token grant_type=client_credentials client_id=service-account-id client_secret=service-account-secret scope=reports:read invoices:write
Deprecated flows - don't use these
- Implicit flow - returned the access token directly in the URL fragment after redirect. Deprecated because tokens in URLs end up in browser history, referrer headers, and server logs. Use Authorization Code + PKCE instead.
- Resource Owner Password Credentials - the user hands their username and password directly to the client, which exchanges them for a token. Defeats the entire point of OAuth (the user never gives credentials to a third party). Only ever justified for first-party apps migrating off legacy auth; avoid entirely.
Why 2.0?
OAuth 1.0 (2007) required every single API request to be cryptographically signed using HMAC-SHA1 - the client had to construct a signature from the URL, HTTP method, parameters, a nonce, and a timestamp, in a specific order. It was correct but painful to implement and easy to get wrong. Libraries helped, but interoperability was fragile.
OAuth 2.0 (RFC 6749, 2012) dropped all of that. Instead of signing requests, it simply requires HTTPS for transport security - the TLS layer handles confidentiality and integrity, so the token itself just needs to be kept secret. Tokens become bearer tokens: whoever holds the token can use it, like cash. Simpler to implement; the tradeoff is that a leaked token is immediately usable by anyone. See HTTP authentication for how bearer tokens fit into the broader picture of HTTP auth schemes.
OAuth 1.0 is dead. When someone says "OAuth" they mean 2.0. There is no OAuth 3.
Key points
- OAuth 2.0 is authorisation, not authentication - it grants access, it doesn't identify the user.
- Bearer tokens: anyone who has them can use them - keep them out of URLs, logs, and the browser.
- Always validate the
stateparameter on callback to prevent CSRF. - Use Authorization Code + PKCE for all interactive flows. Client Credentials for M2M.
- Request minimum scopes. Short-lived access tokens. Refresh tokens server-side only.
- For identity (who is the user?), you need OIDC on top of OAuth 2.0.