How OAuth 2.0 Works: A Developer's Guide (2026)
OAuth 2.0 is the standard behind "Sign in with Google" and every major API. Here's exactly how it works, with diagrams and code.
Every time you click “Sign in with Google” or connect a third-party app to your GitHub account, OAuth 2.0 is running behind the scenes. It’s the protocol that lets users grant apps access to their data — without handing over their password.
In this guide, you’ll learn exactly how OAuth 2.0 works: the authorization flow step by step, what the different grant types are for, how it compares to OIDC and JWT, and when to use each.
What Is OAuth 2.0?
OAuth 2.0 is an open authorization framework that allows a user to grant a third-party application limited access to their account on another service — without sharing their password.
Think of it like a hotel key card system. Instead of giving a guest your master key (your password), the hotel (authorization server) issues a temporary key card (access token) that only opens specific doors (scopes) for a limited time.
OAuth 2.0 is authorization, not authentication. It answers “what can this app access?” — not “who is this user?” (That’s what OpenID Connect adds on top.)
The Four Actors in OAuth 2.0
Before walking through the flow, it helps to know the four parties involved:
| Actor | What it is | Example |
|---|---|---|
| Resource Owner | The user who owns the data | You, the person logging in |
| Client | The app requesting access | A todo app that wants to read your Google Calendar |
| Authorization Server | Issues tokens after user consents | Google's OAuth server (accounts.google.com) |
| Resource Server | Hosts the protected data | Google Calendar API |
The OAuth 2.0 Authorization Code Flow: Step by Step
The most common and secure OAuth 2.0 flow is the Authorization Code flow. Here’s exactly what happens, step by step:

Step 1: The Client Redirects the User to the Authorization Server
The flow starts when the user clicks “Login with Google” (or similar). The client redirects them to the authorization server with a URL like this:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&response_type=code
&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly
&state=random_csrf_token
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
Key parameters:
client_id— identifies your app to the authorization serverredirect_uri— where to send the user after they approvescope— what access you're requesting (e.g.calendar.readonly). Note: addingopenidto the scope activates OpenID Connect on top of OAuth 2.0 — useful when you also need to identify the user.state— a random value to prevent CSRF attackscode_challenge— part of the PKCE extension (required for public clients)
Step 2: The User Logs In and Grants Consent
The authorization server shows the user a login page and a consent screen — “This app wants access to your email and profile. Allow?” — and the user approves or denies.
Step 3: The Authorization Server Returns an Authorization Code
After the user approves, the authorization server redirects back to your redirect_uri with a short-lived authorization code:
https://yourapp.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token
This code is temporary (usually expires in 60–120 seconds) and can only be used once. It’s not an access token — your server still needs to exchange it.
Step 4: The Client Exchanges the Code for an Access Token
Your server makes a back-channel POST request to the token endpoint — this happens server-side, so the client secret is never exposed to the browser:
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: 'AUTH_CODE_HERE',
redirect_uri: 'https://yourapp.com/callback',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
code_verifier: codeVerifier, // PKCE
}),
});
const { access_token, refresh_token, expires_in } = await response.json();
// Note: refresh_token is optional — not all servers issue one.
// For Google, you need to pass access_type=offline to receive a refresh_token.
The authorization server returns:
- access_token — used to make API requests on behalf of the user
- refresh_token — used to get a new access token when it expires (without the user logging in again). Not always returned — depends on the server and the scopes requested.
- expires_in — how many seconds until the access token expires
Step 5: Use the Access Token to Call the Resource Server
const userInfo = await fetch('https://www.googleapis.com/calendar/v3/users/me/calendarList', {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
const calendars = await userInfo.json();
// { kind: 'calendar#calendarList', items: [...] }
The resource server validates the token and returns the protected data. When the access token expires, use the refresh token to get a new one without prompting the user again.
OAuth 2.0 Grant Types
The Authorization Code flow above is just one of several OAuth 2.0 grant types. Each one is designed for a specific scenario:
| Grant Type | Use Case | Status |
|---|---|---|
| Authorization Code + PKCE | Web apps, mobile apps, SPAs — any user-facing app | ✅ Recommended |
| Client Credentials | Machine-to-machine (M2M) — no user involved | ✅ Recommended |
| Device Code | Smart TVs, CLI tools — devices without a browser | ✅ Recommended |
| Implicit | Old SPAs — token returned directly in redirect | ❌ Avoid — superseded by Auth Code + PKCE |
| Resource Owner Password | App collects username/password directly | ❌ Avoid — superseded by Auth Code + PKCE |
For a deeper explanation of each grant type and when to use them, see our guide on OAuth 2.0 grant types.
OAuth 2.0 vs OpenID Connect (OIDC)
This is one of the most common points of confusion. Here’s the short version:
- OAuth 2.0 handles authorization — "what can this app access?"
- OpenID Connect (OIDC) handles authentication — "who is this user?"
OIDC is built on top of OAuth 2.0. It adds an id_token (a JWT containing user identity info) to the standard OAuth flow, plus a /userinfo endpoint and a standardized openid scope.
In practice: when you add scope=openid to your OAuth 2.0 request, you’re using OIDC. When you only ask for scope=read:email (no openid), you’re using plain OAuth 2.0.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| Purpose | Authorization (access delegation) | Authentication (identity verification) |
| Token returned | Access token | Access token + ID token |
| User info | Not standardized | Standardized /userinfo endpoint |
| Use when | Granting API access to another app | Letting users "log in" to your app |
Most modern implementations use both together. For a deeper comparison with SAML, see OIDC vs SAML. You can also inspect any OIDC provider’s configuration using the OIDC Discovery Endpoint Explorer.
OAuth 2.0 vs JWT
OAuth 2.0 and JWT are often mentioned together but they’re different things:
- OAuth 2.0 is a protocol — it defines how authorization flows work
- JWT (JSON Web Token) is a token format — it defines how to encode claims into a compact, signed string
JWTs are commonly used as OAuth 2.0 access tokens or ID tokens, but OAuth 2.0 doesn’t require JWTs. An OAuth 2.0 access token could be an opaque random string — the resource server just validates it with the authorization server.
When you receive a JWT as your OAuth 2.0 access token, you can decode and verify it locally without making a network call. When you receive an opaque token, you must call the authorization server’s introspection endpoint to validate it.
Scopes: Controlling What Access Is Granted
Scopes define exactly what the client is allowed to do. They’re space-separated strings in the authorization request:
scope=openid email profile calendar.readonly
The user sees these scopes on the consent screen and can approve or deny. The access token issued is then limited to those scopes — even if the resource server would otherwise allow more.
Good scope design follows the principle of least privilege: request only what you actually need. Requesting calendar.readonly instead of calendar signals to users that you won’t modify their calendar.
PKCE: Required for Public Clients
If your client is a mobile app, single-page app (SPA), or any application where you can’t safely store a client secret, you must use PKCE (Proof Key for Code Exchange). Public clients can’t store secrets because their code ships to the user’s device — anyone can inspect a mobile app bundle or browser JavaScript to extract a hardcoded secret.
PKCE prevents authorization code interception attacks by having your client generate a random code_verifier, hash it into a code_challenge, and send the challenge with the authorization request. When exchanging the code for a token, your client sends the original verifier — proving it’s the same client that started the flow.
See our detailed guide on how PKCE works in OAuth 2.0.
Common OAuth 2.0 Mistakes to Avoid
- Not validating the
stateparameter — always verify it matches what you sent to prevent CSRF attacks - Storing access tokens in localStorage — use httpOnly cookies or server-side sessions; localStorage is accessible to JavaScript and vulnerable to XSS
- Using the Implicit grant for SPAs — use Authorization Code + PKCE instead; the Implicit grant is deprecated for good reason
- Not rotating refresh tokens — refresh token rotation means the server issues a new refresh token every time one is used, invalidating the old one. This way, if a refresh token is stolen and used by an attacker, the next legitimate use by your app will detect the mismatch and revoke the session.
- Requesting overly broad scopes — only request what you need; users are more likely to approve narrow, specific permissions
Implementing OAuth 2.0 Without the Complexity
Implementing OAuth 2.0 from scratch means handling token storage, refresh logic, PKCE, state validation, scope management, and security edge cases. Most teams are better served by an authentication platform that handles this for them.
Authgear provides a fully OAuth 2.0 and OIDC compliant authorization server with pre-built login UI, token management, refresh rotation, and support for social login providers (Google, Apple, Facebook) out of the box. You get a production-ready OAuth 2.0 implementation without building the infrastructure yourself.
Summary
Here’s what you need to remember about how OAuth 2.0 works:
- OAuth 2.0 delegates access — users grant apps permission to act on their behalf without sharing passwords
- The Authorization Code + PKCE flow is the correct choice for almost all user-facing apps in 2026
- Access tokens are short-lived; refresh tokens let you get new ones silently
- OAuth 2.0 handles authorization; add OIDC (
scope=openid) if you also need authentication - JWT is a token format often used with OAuth 2.0, not a replacement for it