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.

8
 min. read
March 31, 2026
Star us on GitHub and stay updated

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 server
  • redirect_uri — where to send the user after they approve
  • scope — what access you're requesting (e.g. calendar.readonly). Note: adding openid to 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 attacks
  • code_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 state parameter — 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
Preferences

Privacy is important to us, so you have the option of disabling certain types of storage that may not be necessary for the basic functioning of the website. Blocking categories may impact your experience on the website.

Accept all cookies

These items are required to enable basic website functionality.

Always active

These items are used to deliver advertising that is more relevant to you and your interests.

These items allow the website to remember choices you make (such as your user name, language, or the region you are in) and provide enhanced, more personal features.

These items help the website operator understand how its website performs, how visitors interact with the site, and whether there may be technical issues.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.