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.

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:

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:

  1. The state matches what you generated in step 1. If it doesn't, reject the request.
  2. 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

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:

Two rules of scope hygiene:

  1. Ask for the minimum. If you only need email and name, don't request Gmail access. Users notice broad scopes and consent rates drop.
  2. 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:

  1. Your app generates a random code_verifier (a long random string).
  2. It hashes that value (SHA-256) to get a code_challenge.
  3. It includes the challenge in the initial auth URL.
  4. When exchanging the code for a token, it sends the original verifier.
  5. 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:

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:

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 →