What is OAuth 2.0? A simple guide for junior developers
OAuth 2.0 is one of those topics that looks impossible from the outside and is actually pretty mechanical once you've seen it once. Universities don't teach it. Most tutorials over-explain. By the end of this guide you'll know what's happening when a user clicks "Log in with Google," what tokens are flying around, and the security mistakes that get junior developers in trouble.
The 30-second version
OAuth 2.0 is the standard way for one app (yours) to ask permission to act on a user's behalf with another service (Google, GitHub, Stripe), without the first app ever seeing the user's password. The user logs in directly with the service, the service hands back a token, and your app uses that token to do things on the user's behalf (read their email, post to their feed, charge their card, etc.).
That's the whole concept. Everything else is plumbing.
The four roles
Every OAuth flow has four named roles. Knowing them makes the spec readable.
- Resource Owner. The user. The human whose data we want to access.
- Client. Your app, the one that wants to access the user's data.
- Authorization Server. The service that authenticates the user and hands out tokens. For "Log in with Google," that's Google's OAuth server.
- Resource Server. The API that actually has the data. For Google, this is Google's APIs (Gmail, Drive, etc.). Often the auth server and resource server are run by the same company, but they're conceptually different.
The four-step flow (Authorization Code grant)
This is the flow you'll use 95% of the time. It's also called the "Authorization Code" flow or "auth code" flow. Step by step:
Step 1: User clicks "Log in with Google" in your app
Your app redirects them to Google with a URL like:
https://accounts.google.com/o/oauth2/v2/auth
?client_id=YOUR_APP_ID
&redirect_uri=https://your-app.com/auth/callback
&response_type=code
&scope=openid%20email%20profile
&state=RANDOM_STRING
The important pieces:
client_id: how Google identifies your app. You get this when you register your app with Google.redirect_uri: where Google should send the user back. You pre-register this with Google, so attackers can't redirect to a malicious site.scope: what permissions your app is requesting (read email, post to drive, etc.).state: a random string your app generates and remembers. Used to defend against CSRF attacks; we'll come back to it.
Step 2: User authenticates with Google
Google shows their familiar login page. The user types their password (or uses a passkey, or has a session cookie already). Then Google shows a consent screen: "App X wants to read your email. Approve?"
Critical detail: your app never sees the user's password. That's the whole point of OAuth. The user gives credentials only to Google, who they already trust.
Step 3: Google redirects back with an authorization code
If the user approves, Google sends them back to your redirect_uri:
https://your-app.com/auth/callback?code=AUTH_CODE_HERE&state=RANDOM_STRING
Two things to verify on your end:
- The
statematches what you generated in step 1. If it doesn't, reject the request. - You have a fresh
code, but you don't trust it yet. It's an intermediate value, not the actual access token.
Step 4: Your server exchanges the code for an access token
This is the only part that should happen server-side, never in browser JavaScript. Your backend makes a POST to Google's token endpoint:
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
code=AUTH_CODE_HERE
&client_id=YOUR_APP_ID
&client_secret=YOUR_APP_SECRET
&redirect_uri=https://your-app.com/auth/callback
&grant_type=authorization_code
Google verifies your app's identity (with the secret) and the code, then responds:
{
"access_token": "ya29.a0Af...",
"expires_in": 3600,
"refresh_token": "1//0gB...",
"id_token": "eyJhbGciOiJ...",
"token_type": "Bearer"
}
Now you have an access token. From this point on, you make API calls to Google with this token in the Authorization: Bearer header, and Google does the requested work on the user's behalf.
Access token vs refresh token vs ID token
- Access token. Short-lived (typically 15 to 60 minutes). Sent on every API request:
Authorization: Bearer ya29.a0Af.... Treat it like a password while it's alive. - Refresh token. Long-lived (days, weeks, or months). Used only when the access token expires, to get a new one without re-prompting the user. Critical to store securely; if leaked, an attacker has long-term access.
- ID token. Only in OpenID Connect, not raw OAuth. A JWT (JSON Web Token) that contains the user's identity (email, name, ID). You verify the signature, decode it, and now you know who the user is.
What is OpenID Connect (OIDC)?
OAuth 2.0 alone is about authorization: "this token can read your email." It doesn't actually tell your app who the user is. OpenID Connect (OIDC) is a small layer on top of OAuth that adds the missing identity bit, mainly through the id_token.
"Log in with Google," "Sign in with Apple," and most modern auth systems use OIDC. When you decode the id_token, you get fields like sub (user ID), email, name, picture. That's what your app uses to create or look up the user record.
If someone says "we use OAuth for login," they almost always mean OIDC.
Practice fixing real auth bugs
InternQuest's Security track includes real-shaped OAuth and JWT bugs from the OWASP playbook: hardcoded secrets, wrong status codes on auth failure, missing token validation. Free virtual SWE internship simulator, no setup.
Try a security mission →Scopes
A scope is a string that describes a specific permission. Examples:
openid email profile: identity only (Google login)https://www.googleapis.com/auth/gmail.readonly: read but not modify Gmailhttps://www.googleapis.com/auth/drive.file: access only files your app created
Two rules of scope hygiene:
- Ask for the minimum. If you only need email and name, don't request Gmail access. Users notice broad scopes and consent rates drop.
- Ask incrementally. Don't ask for every scope on first login. Ask for basic identity at signup, then ask for additional permissions only when the user needs the feature.
PKCE: the extra step for mobile and SPAs
The basic auth code flow assumes your app has a server with a secret it can keep secret. But mobile apps, single-page apps (SPAs), and CLI tools can't store a secret safely (anyone can disassemble the app or open DevTools).
PKCE (Proof Key for Code Exchange, pronounced "pixie") is a small extension that fixes this. Instead of a static client secret:
- Your app generates a random
code_verifier(a long random string). - It hashes that value (SHA-256) to get a
code_challenge. - It includes the challenge in the initial auth URL.
- When exchanging the code for a token, it sends the original verifier.
- The auth server hashes the verifier and compares to the challenge. If they match, the request is legitimate.
For new SPAs, mobile apps, and CLIs, always use Authorization Code with PKCE. The OAuth 2.0 Security Best Current Practice now recommends PKCE for all clients, not just public ones.
The flows you don't need to learn (for now)
OAuth 2.0 has multiple "grant types." For 95% of work, you only need Authorization Code (with PKCE for SPAs/mobile). The others:
- Implicit flow. Deprecated. Old SPA pattern, replaced by Auth Code + PKCE.
- Resource Owner Password Credentials. The user types their password into your app. Defeats the entire point of OAuth. Used only for legacy migrations.
- Client Credentials. Server-to-server auth where there's no user. Common for backend integrations (your service calls Stripe's API). Just two parties, no user consent involved.
- Device Code. For TVs, set-top boxes, things without keyboards. User goes to a URL on their phone and types a short code.
You'll meet Client Credentials and Auth Code as an intern. Skip the rest until you need them.
Security mistakes juniors make
1. Hardcoding the client secret in source code
The client secret is the password your app uses with the OAuth server. Hardcoding it in code (especially client-side) leaks it to anyone with view access to your repo or browser. Always read from an environment variable. See our env-var guide.
2. Skipping the state parameter
The state param defends against a CSRF attack where someone tricks a logged-in user's browser into making the auth callback. Generate a random state, store it in the user's session, verify it matches when the redirect comes back. Don't skip this.
3. Validating tokens on the client
Decoding a JWT (ID token) in browser JS to "verify" it is not verification. Anyone can mint a JWT; the signature is what proves it came from the auth server. Always verify signatures server-side using the OAuth provider's published keys (the JWKS endpoint).
4. Storing tokens in localStorage
localStorage is readable by any JavaScript on the page, including from a successful XSS attack. Modern recommendation: store tokens in HTTP-only secure cookies (immune to XSS), or in memory + a refresh-token-rotation pattern.
5. Not handling refresh-token rotation
Modern OAuth providers issue a new refresh token each time you use the old one. If you keep using the old refresh token, it gets revoked. Handle the rotation: store the new refresh token every time.
6. Trusting the redirect URI loosely
If you allow wildcards in your registered redirect URIs, you've opened a hole. Register exact URIs. Reject anything that doesn't match exactly.
What to know vs Google as needed
Internship-grade OAuth literacy:
- Know cold: the four-step Auth Code flow, what an access token vs refresh token is, the difference between OAuth and OIDC, why client secrets matter.
- Be useful at: reading OAuth provider docs, fixing wrong-scope bugs, debugging redirect-URI mismatches, finding why a token is being rejected (expired? wrong audience? revoked?).
- Google as needed: the exact PKCE math, refresh-token rotation logic, OAuth 2.1, mTLS-bound tokens, DPoP. You don't need these in your first month.
The mental model that sticks
OAuth is a hotel keycard. You don't give the hotel your home keys (your password) and you don't get a master key (full account access). You get a card that opens specific doors (scopes) for a specific time (token expiry). When the card stops working, you go to the front desk (refresh token) for a new one. Lose the card, the hotel can deactivate it (token revocation).
Once that mental model clicks, the rest of the spec mostly falls out.
Drill auth and security on real broken code
InternQuest's Security track has 37+ real-shaped missions: JWT verification bugs, OAuth misuse, OWASP Top 10 patterns. Browser-based, free virtual software engineering internship simulator.
Try a security mission →