# OAuth 2.0 Explained for Developers
Every time you click "Sign in with Google" or let an app access your GitHub repos, you're using OAuth 2.0. It's the industry-standard protocol for delegated authorization — letting one application access resources on a user's behalf without ever seeing that user's password. It's also genuinely confusing the first time, partly because it's solving a subtler problem than people expect.
The problem OAuth solves
Suppose a photo-printing app wants to access your Google Photos. The naive approach: give the app your Google password. Terrible idea — the app now has *full* access to your entire account forever, and you can't revoke just that app.
OAuth solves this: the app gets a *scoped, revocable token* that grants access to *only* your photos, *only* the permissions you approved, *without* ever seeing your password. That's delegated authorization — and it's about *authorization* (what you can access), not strictly *authentication* (who you are). That distinction matters and we'll return to it.
The four roles
OAuth defines four parties:
| Role | Who it is | Example |
|---|---|---|
| Resource Owner | The user who owns the data | You |
| Client | The app requesting access | The photo-printing app |
| Authorization Server | Issues tokens after the user consents | Google's OAuth server |
| Resource Server | Holds the protected data | Google Photos API |
The whole dance coordinates these four so the Client gets a token from the Authorization Server to call the Resource Server — without the user's credentials ever touching the Client.
The Authorization Code flow (with PKCE)
This is the main flow you should use for web and mobile apps. Here's the sequence:
1. Client redirects user to the Authorization Server
(with client_id, redirect_uri, scope, state, code_challenge)
2. User authenticates and consents ("Allow this app to view your photos?")
3. Auth Server redirects back to redirect_uri with a short-lived
authorization code
4. Client exchanges that code (+ code_verifier) at the token endpoint
→ receives an access token (and maybe a refresh token)
5. Client calls the Resource Server with the access tokenWhy the two-step (code, then token exchange)? The authorization code travels through the user's browser (visible in the redirect URL), but it's useless on its own — exchanging it for a token happens in a separate, more secure back-channel request. The token itself never appears in a browser URL.
PKCE (Proof Key for Code Exchange, RFC 7636) hardens this further. The client generates a random code_verifier, sends its hash (code_challenge) in step 1, and the actual verifier in step 4. This proves the same client that started the flow is finishing it — defeating code-interception attacks. PKCE is now recommended for *all* clients, not just mobile.
Scopes: least privilege for access
Scopes define *what* the token can do:
scope=photos.read
scope=repo:status read:userThe app requests only the scopes it needs, the user sees them on the consent screen, and the token is limited to them. Request the minimum — over-asking spooks users and increases blast radius if the token leaks.
Access tokens and refresh tokens
Like JWT auth, OAuth typically issues two tokens:
- Access token — short-lived, sent with each API call (often
Authorization: Bearer ...). May be a JWT or an opaque string. - Refresh token — long-lived, used only to get new access tokens without re-prompting the user. Stored securely; revocable.
When the access token expires, the client uses the refresh token to obtain a new one silently.
Which grant type to use
OAuth has several "grant types" (flows). Modern guidance has narrowed the recommendations considerably:
| Grant | Use for | Status |
|---|---|---|
| Authorization Code + PKCE | Web, SPA, mobile apps | ✅ Recommended |
| Client Credentials | Machine-to-machine (no user) | ✅ Recommended |
| Device Code | Devices with no browser (TVs, CLIs) | ✅ For its niche |
| Implicit | (legacy SPAs) | ❌ Deprecated — use Code+PKCE |
| Resource Owner Password | (app collects password directly) | ❌ Avoid |
The [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics) deprecates the Implicit and Password grants. If a tutorial tells you to use Implicit flow, it's out of date.
OAuth is not authentication (use OIDC for that)
This is the most important conceptual point. OAuth 2.0 is about authorization — granting access to resources. It does *not*, by itself, reliably tell the client *who the user is*. Using a raw OAuth access token as proof of identity is a known anti-pattern (the "confused deputy" / token-substitution problems).
For *authentication* — "Sign in with X" — you want OpenID Connect (OIDC), a thin identity layer built on top of OAuth 2.0. OIDC adds an ID token (a JWT with verified identity claims like sub, email, name) and a standardized userinfo endpoint. So:
- OAuth 2.0 → "this app may access your photos."
- OIDC → "this is who the user is."
When you implement "Sign in with Google," you're really using OIDC.
Common pitfalls
- Skipping the
stateparameter. It's your CSRF protection for the redirect — generate it, store it, verify it on return. Omitting it opens CSRF on the callback. - Loose
redirect_urimatching. Register exact redirect URIs; open redirects let attackers steal codes. - Treating access tokens as identity. Use OIDC ID tokens for identity, not access tokens.
- Not using PKCE. It's cheap and closes real attacks; use it everywhere.
- Storing tokens insecurely (e.g. SPA tokens in
localStorageexposed to XSS). - Requesting excessive scopes. Least privilege.
A practical example
For a typical web app adding "Sign in with GitHub," you'd: register an OAuth app to get a client_id/client_secret and set the redirect URI; redirect users to GitHub's authorize endpoint with your scopes, a random state, and a PKCE challenge; handle the callback, verify state, exchange the code for tokens; then call the GitHub API with the access token. The client_secret lives server-side as an environment variable — never in client code.
OAuth and PandaStack
PandaStack supports SSO for accessing the platform, and it gives your *own* apps the building blocks to implement OAuth/OIDC correctly: environment variables for your client_id/client_secret and signing keys (kept out of source control), automatic SSL so authorization codes and tokens are never sent in plaintext, managed Redis (via KubeBlocks) for storing state/PKCE values or refresh tokens, and edge functions if you want to handle OAuth callbacks close to the user. You write the flow; the platform keeps the secrets and transport safe.
References
- [RFC 6749 — OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749)
- [RFC 7636 — PKCE](https://www.rfc-editor.org/rfc/rfc7636)
- [OAuth 2.0 Security Best Current Practice (IETF draft)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics)
- [OpenID Connect](https://openid.net/developers/how-connect-works/)
- [oauth.net — overview](https://oauth.net/2/)
Implementing OAuth in your app? Keep your client secrets in env vars and ship over automatic SSL with PandaStack's free tier. [Start at dashboard.pandastack.io](https://dashboard.pandastack.io).