Core idea
OAuth2 solves delegated authorization: you let an app act on your behalf without handing it your password. The app gets a scoped access token, not your credentials.
Authorization-code flow
- The app redirects the browser to the authorization server with
client_idand a redirect URI; - You log in there and consent;
- The server redirects back with a one-time authorization code;
- The app exchanges the code plus
client_secretfor an access token (server-side, never in the browser).
Two tokens
| Token | Use | Lifetime |
|---|---|---|
| access token | Call APIs | Short |
| refresh token | Get new access tokens | Long |
Why PKCE
Mobile and SPA apps cannot keep client_secret safe. PKCE replaces the secret with a one-time challenge: an attacker who intercepts the code still cannot trade it without the challenge.
Real-world cases: three authorization-code mistakes
- Storing access_token in the frontend: one XSS steals it. The steadier pattern is for the frontend to hold only the code, with the backend exchanging it — or PKCE for public clients.
- Not validating state on the callback: an attacker can craft a callback that binds the victim's account to their own authorization (login CSRF).
statemust be generated once and verified against the session. - Wildcard redirect_uri: an open redirect can deliver the authorization code to an attacker's domain. Callback URLs must be an exact, per-entry allowlist — no prefix or wildcard matching.
FAQ
Why exchange the code for a token at all? The code appears only briefly in the frontend and must be exchanged by the backend with client_secret, keeping tokens out of browser history and logs. What does PKCE solve? It lets public clients that cannot keep a client_secret (mobile apps, SPAs) use the authorization-code flow safely. Is OAuth2 an authentication protocol? It handles authorization; authentication layers on OpenID Connect's id_token. Where should a refresh_token live? Server-side only, with rotation and revocation.
Operating the authorization server and tokens
Once OAuth2 is integrated, the long-term cost sits in token and client management:
- Record every client registration: callback URLs, secrets and owners, with secret rotation and an overlap window so rotation is not an outage;
- Tiered token lifetimes: short access tokens (minutes) and longer refresh tokens that can be revoked, rotated on each refresh to shrink the theft window;
- Minimal scopes: request only what is needed, and let users review and revoke authorised apps at any time;
- Anomaly detection: the same refresh token used in two places, or a burst of exchanges, may signal compromise — alert and support one-click revocation;
- Logout must cascade: revoke tokens on logout instead of only clearing frontend storage, or the token stays valid.
Keep authorization codes short-lived (30–60 seconds) and single-use; a reused code should be treated as a leak and invalidate all tokens from that grant.
Connecting to business permissions
- Separate identity from permission: tokens answer who you are and what you may reach; which records you can change is still a business decision. Do not judge them in one place.
- Authorise at the resource: every resource endpoint must check the caller's right to that resource rather than trusting a valid token, or privilege bugs cluster on a few endpoints.
- Keep volatile permissions out of tokens: embedding permissions delays changes until expiry; systems that adjust access often should query at runtime or use shorter token lifetimes.
- Audit against the token: record the calling identity on all significant operations so you can trace which application performed what.
- Revocation must cascade: when a user revokes a third-party app, invalidate its tokens, not just the stored state.
With clear boundaries, the protocol handles admission and your system handles capability — and privilege investigations get much easier.