OAuth 2.0 sits behind every "Sign in with Google" button. This guide explains what OAuth actually is (delegation, not authentication), the four standard flows, why the implicit flow has been replaced by PKCE, and how refresh tokens and scopes fit in. Based on RFC 6749, RFC 7636 (PKCE), and current best practice.
What OAuth solves
Imagine an email-marketing app that wants to read your Gmail contacts. The naïve approach — give it your password — is awful: the app sees your secret and now has every Google permission you do.
OAuth's answer:
- The third-party app asks the user to grant Google access on its behalf.
- The user goes to Google, signs in, and approves "read contacts only".
- Google issues an access token to the app.
- The app calls the contacts API with the token. It never sees the password.
The key word is delegation. The user delegates a slice of their permissions to a third party. OAuth itself is not authentication — for that you layer OpenID Connect (identity information on top of OAuth).
Four roles
- Resource Owner — the user (the data's owner).
- Client — the app borrowing access (the marketing tool).
- Authorization Server — issues tokens (Google's OAuth server).
- Resource Server — the protected API (Google Contacts API).
IdPs like Google, GitHub, and Auth0 act as Authorization Server and Resource Server at once.
The four standard grant types
1. Authorization Code (recommended for server-side apps)
The most common flow. Web apps with a backend.
- The client redirects the user to the Auth Server's
/authorizeendpoint withresponse_type=code,client_id,redirect_uri,scope, andstate. - After consent, the Auth Server redirects to
redirect_uri?code=...&state=.... - The client (server) POSTs to
/tokenexchanging the code plusclient_secretfor an access token (and optionally a refresh token). - The client calls the API with the access token (
Authorization: Bearer ...).
Why it's good: client_secret stays on the server; the token never appears in a browser URL.
The state parameter defends against CSRF — generate a random value and verify it on callback. URL Parser pulls apart the callback URL so the flow becomes obvious.
2. Authorization Code + PKCE (recommended for SPAs and mobile)
SPAs and mobile apps can't keep client_secret safe — bake it into the bundle and it's extracted in minutes. PKCE (Proof Key for Code Exchange, RFC 7636) is the answer.
- The client generates a
code_verifier(random 43–128 chars) and computescode_challenge = SHA256(code_verifier). - On
/authorize, sendcode_challenge+code_challenge_method=S256. - On the
/tokenexchange, send the originalcode_verifier. The Auth Server SHA-256s it and checks it matches the challenge.
Now even if an attacker steals the code, they can't exchange it without the code_verifier. Safe code exchange without a client secret. Want to feel the SHA-256 yourself? Drop the verifier into SHA Hash and compare against the challenge.
3. Client Credentials (server-to-server)
No user involved. A cron job calls an API with its own permissions. The client posts directly to /token with grant_type=client_credentials + client_id + client_secret and gets an access token.
Scopes are limited to those pre-granted to the client. Use this for application-owned data (analytics, admin APIs), not user data.
4. Refresh Token
Strictly speaking a separate grant for refreshing access tokens.
POST /token
grant_type=refresh_token
refresh_token=<refresh_token>
client_id=...Access tokens are short-lived (5–60 min); refresh tokens last days or weeks. When access expires, exchange refresh for a new access (and ideally rotate the refresh too). Store refresh tokens in secure storage — HttpOnly cookies, OS keychains.
(deprecated) Implicit and Resource Owner Password
Both removed in the OAuth 2.1 draft. Implicit exposes the token in the URL fragment — vulnerable to XSS/CSRF. Password Grant gives the client the user's password, defeating the entire point of OAuth. Don't use either in new systems.
Scope — units of permission
The scope parameter declares what's being delegated. Provider-specific:
- Google:
https://www.googleapis.com/auth/contacts.readonly - GitHub:
repo,user:email,workflow - OpenID Connect:
openid profile email
Principle: least privilege. Ask only for what you need. Shorter consent screens convert better too.
Access tokens come in two shapes
JWT (self-contained)
The token itself contains the user info and permissions. Resource servers verify with a library — try JWT Decoder to peek inside one. No DB lookup needed. Used by Auth0, Cognito, Firebase.
Want to issue one yourself? JWT Encoder (HMAC) takes payload + secret + algorithm. The HS256 signature is exactly what HMAC Generator produces.
Opaque (introspection)
The token is just an opaque random string. The resource server asks the auth server's /introspect endpoint on every request. Lets the auth server revoke instantly (pro), at the cost of a lookup per request (con).
OpenID Connect — OAuth + identity
OAuth only handles authorization. To know who the user is, layer on OIDC.
scope=openidactivates OIDC.- In addition to the access token, the server returns an ID token (a JWT) with standard claims like sub, email, name.
- The
/userinfoendpoint returns extended user info.
That's what "Sign in with Google" really is — OIDC. Your app receives Google's ID token and creates a session for the user.
Common pitfalls
1. Skipping state verification
Without verifying state on callback, an attacker can redirect the user with the attacker's code and bind their OAuth account to the user's session. Always store on issue, compare on return.
2. SPAs without PKCE
Embedding client_secret in a SPA leaks it instantly. PKCE generates a fresh code_verifier per flow and is safe even without a secret. Always require PKCE for SPAs.
3. Storing tokens in localStorage
One XSS and every token is gone. Prefer HttpOnly Secure cookies, or keep access in memory and only the refresh in a cookie.
4. Wildcard redirect_uri
Register exact URIs at the auth server. Wildcards like example.com/* become open redirects — and open redirects leak tokens.
5. Over-requesting scopes
"Just ask for everything" creates a scary consent screen and users bounce. Request scopes incrementally as features need them.
6. No refresh token rotation
Best practice: every refresh issues a new refresh token and invalidates the previous one. If the same refresh token is used twice you've got an attacker — and you can detect it.
Concrete example — Google OAuth
- Create an OAuth client in Google Cloud Console → get client_id + secret. Register a redirect_uri.
- On click, redirect users to
https://accounts.google.com/o/oauth2/v2/authwith the standard query parameters. - On callback, exchange
codeathttps://oauth2.googleapis.com/token. - Call
https://www.googleapis.com/oauth2/v3/userinfowith the access token.
Libraries — Auth.js (NextAuth), Passport, Authlib (Python), Spring Security — abstract every step.
Summary
- OAuth 2.0 = delegation. Authentication needs OIDC on top.
- Server apps → Authorization Code. SPAs/mobile → + PKCE. Server-to- server → Client Credentials.
- Implicit and Password grants are deprecated. Don't use them.
- Short access tokens; refresh to renew. Prefer HttpOnly cookies.
- Minimum scopes.
state+ PKCE + exact redirect URI is the baseline defense.