Skip to main content
TF
By Rohit V.9 min readArticle

What Is OAuth 2.0? A Developer's Plain-English Guide

TF
ToolsFuel Team
Web development tools & tips
Padlock icon on a glowing digital screen representing authentication security

Photo by Towfiqu barbhuiya on Unsplash

"Just Use OAuth" — Sure, But What Even Is It?

About three years ago I was building my first API integration — a side project that needed to pull data from a user's Google Calendar. My senior dev at the time just said, "use OAuth," and walked away. Super helpful.

I stared at the Google OAuth docs for a solid two hours. Redirect URIs. Authorization codes. Access tokens. Refresh tokens. Scopes. Clients and servers and resource owners. It's not that any one piece is complicated — it's that the terminology is dense and the official documentation assumes you already know half of it.


So here's the explanation I wish someone had given me. No spec-speak, no abstract diagrams. Just how OAuth 2.0 actually works, why it exists, and when you'd use each part of it.


OAuth 2.0 is an
authorization framework — not an authentication protocol, despite being used for both. That distinction matters more than it sounds. Authorization says "what can you access?" Authentication says "who are you?" OAuth 2.0 is primarily designed to answer the first question. The second question gets bolted on via OpenID Connect, which is a layer on top of OAuth 2.0. We'll get there.

Why OAuth Exists — The Problem It Solves

Before OAuth, if you wanted an app to access your email, you'd hand over your email password. Literally. The app would store it, log in as you, and do whatever it needed to do.

That's obviously terrible. If the app gets hacked, your password is compromised. If you want to revoke the app's access, you'd have to change your password — which breaks every other app using it. You have zero visibility into what the app is actually doing with your credentials.


OAuth solves this by introducing a
middleman token system. Instead of giving the app your password, you tell the service "hey, this app is allowed to read my calendar — give it a time-limited token to do that." The app never sees your password. The token has limited permissions (called scopes). You can revoke it any time without touching your password.

Here's the cast of characters in every OAuth interaction:


-
Resource Owner — you, the user who owns the data - Client — the app that wants access to your data - Authorization Server — the service that issues tokens (Google's OAuth server, GitHub's OAuth server, etc.) - Resource Server — the API that holds the actual data (Google Calendar API, GitHub API)

In small services, the Authorization Server and Resource Server are often the same thing. At big companies, they're usually separate.

The Authorization Code Flow — What Actually Happens

Server rack illuminated in blue light representing backend authentication infrastructure

Photo by Thomas Jensen on Unsplash

There are four OAuth 2.0 flows (grant types), but the Authorization Code Flow is the one you'll use 90% of the time for web apps. Walk through it once and the rest make sense by comparison.

Step 1: The redirect Your app redirects the user to the Authorization Server's login page. The redirect URL includes: - Your app's `client_id` - The `redirect_uri` where the user should come back after authorizing - The `scope` of permissions you're requesting (e.g., `read:calendar`) - A `state` parameter — a random string you generate to prevent CSRF attacks - `response_type=code` — tells the server you want an authorization code back

That URL looks something like: `https://accounts.google.com/o/oauth2/auth?client_id=YOUR_ID&redirect_uri=https://yourapp.com/callback&scope=calendar.readonly&response_type=code&state=xyz123`


Step 2: The user consents The user logs in (to Google, GitHub, wherever) and sees a permissions screen: "YourApp wants to read your calendar. Allow or Deny?" They click Allow.

Step 3: The authorization code arrives Google redirects the user back to your `redirect_uri` with an authorization code in the query string: `https://yourapp.com/callback?code=4/P7q7W91a-oMsCeLvIaQm6&state=xyz123`

This code is short-lived — typically expires in a few minutes — and can only be used once. It's not an access token yet.


Step 4: Exchange code for tokens Your server (not the browser — this is important) makes a POST request to the Authorization Server, sending the authorization code plus your `client_secret`. This server-to-server exchange returns: - An access token — used to make API requests - A refresh token — used to get new access tokens when they expire - Token expiry info

Why the two-step code-then-token dance? Because the authorization code travels through the browser (visible in URL logs, referrer headers, browser history). The actual tokens get exchanged server-side over a secure channel. This way, even if someone intercepts the code, they can't use it without your `client_secret`.


Step 5: Use the access token Now your app includes the access token in API requests:

``` GET https://www.googleapis.com/calendar/v3/calendars/primary/events Authorization: Bearer ya29.A0AfH6SMB... ```


The Resource Server validates the token and returns the data.

Access Tokens vs Refresh Tokens — The Difference Matters

Access tokens are short-lived by design. Google's typically expire after 3600 seconds (one hour). GitHub's expire after 8 hours. Why so short? Because tokens can get stolen — from logs, from compromised servers, from memory leaks. If an attacker grabs an access token, limiting its lifespan limits the damage window.

But you don't want users to log in again every hour. That's where refresh tokens come in. A refresh token is a long-lived credential — sometimes valid for months or until revoked — that your server stores securely and uses to obtain new access tokens silently, without any user interaction.


The flow is simple: when an API call fails with a 401 Unauthorized (or when you notice the access token is about to expire), you send the refresh token to the Authorization Server's token endpoint. You get back a fresh access token. The user never sees any of this.


I've messed this up before — storing refresh tokens in a cookie instead of a secure httpOnly cookie, where JavaScript could read it. That's a session hijacking risk. Refresh tokens belong in httpOnly cookies or secure server-side storage. Access tokens can live in memory (not localStorage) on the client if needed.


One more distinction: if you're building a pure API service (no user login, machine-to-machine), look at the
Client Credentials Flow instead. It's basically: send your `client_id` and `client_secret`, get an access token directly. No user consent screen, no redirects. Clean and simple for backend-to-backend calls.

For quick token debugging, you can decode the JWT payload of most OAuth access tokens using a
JWT decoder — just paste the token to see expiry times and scopes. It doesn't verify the signature (you need the secret key for that) but it's great for debugging "why is this token rejected" situations.

OAuth 2.0 vs OpenID Connect vs "Sign in with Google"

Here's where people get confused, and honestly it's not your fault — the marketing doesn't help.

OAuth 2.0 was designed for authorization: "can this app access this resource?" It was never meant to handle "who is this user?" But developers quickly started using it for login anyway, by requesting a profile scope and reading the user's email from the API response. This works, but it's a hack.

OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that properly handles the "who are you?" question. It adds an ID token — a JWT that contains the user's identity (name, email, user ID) — as a first-class concept, separate from the access token. When you implement "Sign in with Google" properly, you're using OIDC.

The practical difference: if you only need to know "this is the user identified by this ID," use OIDC. If you need to access that user's Google Drive files, use OAuth 2.0 (often alongside OIDC). In practice, most major identity providers bundle both.


The `scope=openid` parameter is the toggle. Add it to your OAuth request and the Authorization Server includes an ID token in the response. Add `scope=openid email profile` and you get their email and name too — without making a separate API call to the userinfo endpoint.


For a working example,
Google's OAuth 2.0 Playground lets you walk through the entire flow interactively, try different scopes, and inspect the raw token responses. I spend more time there than I'd like to admit when debugging auth issues.

PKCE — Why the Flow Changed for SPAs and Mobile Apps

Remember the Authorization Code Flow requires a `client_secret` for the token exchange? SPAs and mobile apps have a problem: there's no secure place to store a `client_secret`. If it's in your JavaScript bundle or your mobile app binary, anyone can extract it. A `client_secret` that everyone can read is... not a secret.

The solution is
PKCE (Proof Key for Code Exchange, pronounced "pixie"). Instead of a `client_secret`, your app generates a random `code_verifier` string before the redirect, hashes it into a `code_challenge`, and sends the challenge with the initial authorization request. When exchanging the authorization code for tokens, the app sends the original `code_verifier`. The Authorization Server hashes it and confirms it matches the earlier challenge — proving the entity doing the token exchange is the same one that started the flow.

This prevents authorization code interception attacks. Even if someone steals the code (via a malicious redirect URI, for instance), they can't exchange it without the `code_verifier` that only your app generated.


As of 2019, PKCE is recommended for all OAuth clients — even those that do have a `client_secret`. The
RFC 7636 spec has the full details, but the short version: always use PKCE, full stop. Any OAuth library worth using in 2026 will have built-in PKCE support.

If you're parsing OAuth response data that comes back as JSON, the
JSON formatter makes debugging those token responses a lot easier — especially when you're comparing access token payloads across environments. And if you're decoding Base64-encoded token payloads manually, the Base64 decoder handles that in one paste.

Common OAuth Mistakes That'll Bite You Later

I've made most of these myself, which is the only reason I know to warn you about them.

Not validating the `state` parameter. After the redirect callback, your app must check that the `state` value matches what you sent originally. Skipping this check makes you vulnerable to CSRF attacks. It's one line of code. Do it.

Using the Implicit Flow. This was the old approach for SPAs — it returned the access token directly in the URL fragment, no code exchange needed. It's been deprecated since OAuth 2.0 Security Best Current Practice (2019). The token appearing in the URL is the problem: it ends up in logs, history, and referrer headers. Use Authorization Code + PKCE instead.

Storing tokens in localStorage. localStorage is readable by any JavaScript on the page. If you have a single XSS vulnerability anywhere — a third-party script, a library with a security issue — the attacker can steal everything in localStorage. Access tokens should live in memory, refresh tokens in httpOnly cookies.

Requesting more scopes than you need. If your app only needs to read someone's email, don't ask for `write:everything`. Minimal scope is good security practice and also makes users more likely to click Allow on the consent screen.

Not handling token refresh errors. Refresh tokens can expire too, especially if they're idle for months or if the user revokes your app's access. A failed refresh needs to send the user through the authorization flow again. Apps that silently eat this error and then make API calls with an expired access token produce mysterious 401s that are painful to debug.

Leaking tokens in logs. I've seen production systems that logged every request header — including `Authorization: Bearer ...`. Grep your logs before you ship. Token-in-log is one of the most common causes of API credential compromise.

If you're building a backend that issues its own JWTs as part of an OAuth-adjacent system, the earlier post on
what JWT tokens actually contain covers the payload structure you'll be working with — especially useful if you're validating tokens across microservices.

Frequently Asked Questions

What's the difference between OAuth 2.0 and OAuth 1.0?

OAuth 1.0 required cryptographic signatures on every request — each API call had to be signed using HMAC-SHA1 with your client secret. OAuth 2.0 dropped that requirement by relying on HTTPS for transport security instead, making it simpler to implement but also requiring TLS everywhere. OAuth 2.0 also introduced cleaner separation between grant types (flows) for different use cases. OAuth 1.0 is effectively obsolete — almost no major provider supports it anymore.

Is OAuth 2.0 the same as OpenID Connect?

Not quite. OAuth 2.0 is an authorization framework that answers 'can this app access this resource?' OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that answers 'who is this user?' When you click 'Sign in with Google,' you're using OIDC. The key addition is the ID token — a JWT carrying the user's identity — which OAuth 2.0 alone doesn't define. Many apps use both together: OIDC for login, OAuth 2.0 for API access.

Can I use OAuth without HTTPS?

Not safely. OAuth 2.0's security model depends entirely on HTTPS to protect tokens in transit. Without TLS, tokens can be intercepted in network traffic. The OAuth 2.0 specification explicitly requires that the authorization endpoint and token endpoint use HTTPS. Any test setup using HTTP localhost is the only acceptable exception — and only in development, never production.

What is an OAuth scope and how do I choose which ones to request?

A scope is a permission string that specifies what your app is allowed to access. Examples: 'read:user' on GitHub means read-only access to public profile info; 'calendar.readonly' on Google means read-only Calendar access. Always request the minimum scopes needed for your feature — don't ask for write permissions if you only read. Broader scopes make users less likely to approve your app and increase the damage if a token is compromised.

How do I revoke an OAuth token?

Most Authorization Servers expose a revocation endpoint (often /oauth/revoke or /token/revoke) that accepts your access or refresh token via a POST request. Revoking the refresh token invalidates future access token renewals. The existing access token may still work until it expires naturally — that's a limitation of stateless token systems. From the user's side, they can usually revoke your app's access through the service's security settings (Google: myaccount.google.com/permissions, GitHub: Settings > Applications).

What's the quickest way to test an OAuth flow without building a full app?

Google's OAuth 2.0 Playground (developers.google.com/oauthplayground) is the fastest option for Google APIs — it handles the entire flow interactively. For other providers, Postman has a built-in OAuth 2.0 authorization helper that manages redirects and token storage for you. You can also decode the access or ID tokens you receive by pasting them into the [JWT decoder on ToolsFuel](/tools/jwt-decoder) to inspect expiry, scopes, and claims without writing any code.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools