DEV Community

Tyson Cung
Tyson Cung

Posted on

How OAuth 2.0 Actually Works — A Developer's Guide

You click "Sign In with Google" and two seconds later you're logged in. Behind that button is a protocol handling billions of authentications daily — and most developers who implement it don't fully understand what's happening.

The Problem OAuth Solved

Before OAuth, if a third-party app wanted access to your Google data, you'd hand over your actual Google password. The app stored it, used it to log in as you, and had full access to everything. If that app got breached, your Google account was compromised. If you wanted to revoke access, you had to change your password — which broke every other app you'd given it to.

This was the norm until around 2007. Twitter engineers and developers at other companies started sketching an alternative: what if apps could get limited access without ever seeing your password? That became OAuth.

The Four Players

Every OAuth flow has four roles:

Resource Owner — that's you. The human who owns the data.

Client — the application requesting access. Could be a web app, mobile app, or CLI tool.

Authorization Server — the identity provider (Google, GitHub, Auth0) that authenticates you and issues tokens.

Resource Server — the API that holds your data. Sometimes this is the same system as the authorization server, sometimes not.

These four players interact in a carefully choreographed sequence. Mess up any step and you've got a security hole.

Authorization Code Flow (The One You Should Use)

This is the standard flow for web applications and the one you'll implement most often:

  1. Your app redirects the user to the authorization server (e.g., Google's consent screen)
  2. The user logs in and grants permission
  3. The authorization server redirects back to your app with a short-lived authorization code
  4. Your app's backend exchanges that code (plus a client secret) for an access token
  5. Your app uses the access token to call APIs on the user's behalf

The critical security detail: the authorization code is useless on its own. It can only be exchanged for a token by a server that knows the client secret. So even if someone intercepts the code during the redirect, they can't do anything with it.

Tokens: The Keys to the Kingdom

Access tokens are short-lived (usually 15-60 minutes). They're what your app sends with every API request. Think of them as temporary visitor badges — they get you in the door but expire quickly.

Refresh tokens are long-lived. When your access token expires, your backend uses the refresh token to get a new one without bothering the user. The user logs in once; refresh tokens keep the session alive for days or weeks.

Why two tokens? If an access token gets stolen, the damage window is small — minutes, not months. Refresh tokens are stored server-side and never exposed to the browser.

Scopes: Granular Permissions

OAuth doesn't give all-or-nothing access. Scopes let you request specific permissions. When Google shows "This app wants to read your email" — that's a scope (gmail.readonly). The user sees exactly what they're granting and can make an informed choice.

Good scope design follows least privilege: request only what you need. An app that needs your name and email shouldn't ask for access to your entire Google Drive. Users notice, and consent rates drop when you ask for too much.

PKCE: Closing the Mobile Security Gap

The original authorization code flow assumed a confidential server-side client. Mobile and single-page apps can't keep a client secret — the code is right there on the device.

PKCE (Proof Key for Code Exchange, pronounced "pixie") fixes this. Your app generates a random string (code verifier), hashes it (code challenge), and sends the hash with the initial authorization request. When exchanging the code for a token, the app sends the original string. The server verifies the hash matches. An attacker who intercepts the authorization code doesn't have the verifier, so the code is worthless to them.

As of OAuth 2.1 (the latest draft spec), PKCE is required for all clients, not just public ones. It's that effective.

Common Implementation Mistakes

Storing tokens in localStorage. Any XSS vulnerability can steal them. Use httpOnly cookies for web apps, or secure device storage for mobile.

Not validating the state parameter. The state parameter prevents CSRF attacks. Skip it and an attacker can trick a user into linking their account to the attacker's identity. I've seen this vulnerability in production apps from companies that should know better.

Overly broad scopes. Requesting admin access when you need read access. If your token gets compromised, the blast radius is everything instead of just read operations.

Ignoring token expiration. Treating access tokens as permanent. They expire for a reason. Build your token refresh logic from day one, not as an afterthought when users start getting 401s.

Using the Implicit flow. The implicit flow returns tokens directly in the URL fragment. It was designed for an era before CORS. It's deprecated. Use authorization code + PKCE instead. Always.

The Real-World Trade-Off

OAuth adds complexity. There's no getting around it. You're trading simple password-based auth for a multi-step token dance involving redirects, secrets, and expiration management.

The payoff: your users never give you their passwords. You never have to store them. You get granular, revocable access. And when (not if) your app has a security incident, the damage is contained to whatever scopes you requested, with tokens that expire on their own.

For any application touching user data from a third-party service, OAuth isn't optional. It's the baseline. Understand it deeply, implement it carefully, and don't skip the security steps that make it work.

Top comments (0)