OAuth 2.0 explained without the headache
Discover OAuth 2.0 explained without the headache, what problems it solves, and how to make better decisions when using this protocol day to day.
Every time a junior dev implements "login with Google" for the first time, the same confusion happens: they get an access token, decode it, see a pile of information and conclude that's the user's identity. It works in the tests, ships to production, and months later someone discovers the session can be forged because the system was trusting the wrong token to authenticate. The problem isn't the code — it's the mental model. OAuth 2.0 is almost always taught backwards.
This post undoes OAuth's central confusion and shows the decisions that actually matter when you use it. Worth saying up front: OAuth and session authentication solve different problems, and if your question is "do I use a token or a session for login," read JWT vs session, because half of real OAuth mistakes are authentication mistakes in disguise.
OAuth is not authentication. It's delegated authorization.
This is the sentence that resolves 80% of the confusion. OAuth 2.0 was not built to answer "who is this user." It was built to answer "does this application have permission to access this resource on behalf of the user." The difference is enormous.
Think of a concrete scenario: a photo-editing app wants to access your photos in Google Drive. You don't want to give the app your Google password — that would be insane. OAuth solves exactly this: you authenticate with Google, Google asks "can this app see your photos?", you approve, and the app gets a token granting access only to the photos, never touching your password. That's authorization delegation. At no point does the photo app "know who you are" in the authentication sense — it just earned permission to do one specific thing.
When someone uses OAuth to log in and treats the access token as identity, they're using the tool for a problem it wasn't designed to solve. There's a layer on top of OAuth for that — we'll come back to it at the end.
The four roles you need to understand
OAuth defines four actors, and understanding who's who undoes most of the mystery in the flow diagrams:
- Resource owner: you, the user who owns the data.
- Client: the application that wants to access the data (the photo app).
- Authorization server: the one that authenticates the resource owner and issues the tokens (Google's login server).
- Resource server: the API that holds the protected data (the Google Drive API).
The whole flow is a dance between these four: the client sends the user to the authorization server, the user approves, the authorization server issues a token, and the client uses that token to talk to the resource server. Confusing the authorization server with the resource server is the source of half of all configuration bugs.
Authorization code flow with PKCE: the only one that matters today
There are several "grant types" in OAuth, but for modern applications the answer is just one: authorization code flow with PKCE (Proof Key for Code Exchange). The flow works like this:
1. Client generates a random code_verifier and its hash (code_challenge)
2. Client redirects the user to the authorization server, sending the code_challenge
3. User authenticates and approves the scopes
4. Authorization server redirects back with a short authorization code
5. Client exchanges the code + code_verifier for an access token
6. Authorization server checks the verifier against the challenge and issues the token
PKCE exists to solve a specific problem: what if someone intercepts the authorization code in the redirect? Without PKCE, that intercepted code can be exchanged for a token. With PKCE, the code is only valid alongside the original code_verifier, which never traveled across the network in the redirect. It's cheap to implement and closes a real hole. Always use it, including in server-side apps.
Why the implicit flow is dead
If you find an old tutorial recommending the implicit flow, close the tab. The implicit flow delivered the access token directly in the redirect, inside the URL fragment, with no exchange step. It was designed for an era when SPAs couldn't keep any secret and CORS was limited.
The problem is that dumping the token straight into the URL exposes it to leakage through browser history, server logs and referrer headers. Today, with PKCE, a SPA can run the full authorization code flow with no client secret, eliminating the only reason the implicit flow ever existed. The OAuth Security BCP itself recommends against using it. If your lib still offers it, it's legacy.
Access token, refresh token, and the mistake of treating OAuth as login
The access token is short-lived — minutes to a few hours — and it's what you send on the request to the API. The refresh token is long-lived and serves one purpose only: getting a new access token when the current one expires, without bothering the user with a fresh login. Treat them with different sensitivities: the access token you send on every request; the refresh token you guard like gold and never expose on the client if you can avoid it.
When you need to inspect what's inside a token to debug scopes or expiration, a JWT decoder shows the claims instantly — handy for checking whether aud, exp and scope are what you expect. But beware the trap: decoding an access token and reading its claims to authenticate the user is exactly the mistake that opens this post. The access token is for authorizing access to a resource, not for proving identity.
The solution for login is OpenID Connect (OIDC), an identity layer built on top of OAuth 2.0. OIDC adds the id_token, a JWT whose explicit purpose is to carry the user's identity in a verifiable way. If you want "login with Google," what you want is OIDC, not raw OAuth. OAuth gives you permission; OIDC tells you who.
Frequently asked questions
Is OAuth 2.0 for logging in?
Not directly. OAuth is delegated authorization — it gives an app permission to access resources on the user's behalf. For login, use OpenID Connect, an identity layer built on top of OAuth that provides the id_token designed to prove who the user is.
What's the difference between an access token and a refresh token?
The access token is short-lived and accompanies each request to the API. The refresh token is long-lived and serves only to obtain new access tokens when the current one expires, without a fresh login. Protect the refresh token with far more care.
Do I need PKCE even on a server-side app?
Yes. PKCE is no longer SPA-only and is now recommended for all clients, including server-side. It closes the authorization code interception risk at very low implementation cost. There's no reason to skip it.
Why not use the implicit flow anymore?
Because it exposes the access token directly in the redirect URL, vulnerable to leakage via history, logs and referrer. With PKCE, the authorization code flow works in SPAs with no client secret, making the implicit flow obsolete and insecure.
What to take from here
OAuth 2.0's confusion almost always comes from a sentence nobody said at the right time: OAuth is delegated authorization, not authentication. Understand the four roles, use authorization code flow with PKCE and nothing else, treat access and refresh tokens with different sensitivities, and when the goal is login, move up to OIDC instead of torturing an access token to extract identity. The protocol is simple once you stop asking it to solve the wrong problem.
- 01 UTF-8 and encoding problems: why accents break and how to fix them What UTF-8 is, code points, mojibake, and why accented text turns to garbage — the mechanism explained end to end.
- 02 MIT, Apache, and GPL Licenses: Practical Differences for Developers MIT, Apache 2.0, and GPL are not the same thing. Understand permissive vs copyleft, patent protection, and compatibility before shipping code to production.