← Back to Blog

OAuth 2.0 Explained Simply: The Complete Guide

OAuth 2.0 is the authorization framework behind "Sign in with Google," GitHub integrations, and most modern API security. It sounds complicated but the core idea is elegant. This guide explains every part of OAuth 2.0 - the flows, the tokens, the scopes, and the common mistakes - with real examples throughout.

The Problem OAuth 2.0 Solves

Imagine you want a third-party app to read your Google Calendar. Before OAuth, the only option was to give the app your Google username and password. The app would log in as you and read your calendar - but it would also have access to your Gmail, Drive, and every other Google service. There was no way to grant limited access.

OAuth 2.0 solves this by introducing an authorization layer between the user and the application. Instead of sharing credentials, the user grants the application a limited-scope token. The app never sees the password. The token can be scoped to read only calendars. The token can expire or be revoked without changing the password. This is the fundamental insight of OAuth.

The Four Key Roles in OAuth 2.0

  • Resource Owner: The user who owns the data and grants access. When you click "Sign in with Google," you are the Resource Owner.
  • Client: The application requesting access. The third-party calendar app, the GitHub integration, the tool that wants to read your data.
  • Authorization Server: The server that authenticates the user and issues tokens. Google's OAuth server, GitHub's authorization endpoint, your company's identity provider.
  • Resource Server: The API that holds the protected data. Google Calendar API, GitHub API, your backend API. It accepts access tokens and returns data.

The Authorization Code Flow (The Recommended Flow)

The Authorization Code Flow is the correct choice for almost all applications with a server side backend. It is the most secure because the access token is never exposed to the browser - only a short-lived authorization code is, and the code is exchanged for a token in a server-to-server call.

Step-by-Step Walk-Through

  1. User initiates login. The user clicks "Sign in with Google" on your app. Your server generates a random state parameter (CSRF protection) and stores it in the session.
  2. Redirect to Authorization Server. Your app redirects the user's browser to Google's authorization endpoint with these parameters:
    https://accounts.google.com/o/oauth2/v2/auth?
      response_type=code
      &client_id=YOUR_CLIENT_ID
      &redirect_uri=https://yourapp.com/callback
      &scope=openid%20email%20profile%20https://www.googleapis.com/auth/calendar.readonly
      &state=RANDOM_STATE_VALUE
  3. User authenticates and consents. Google shows a login screen (if not already logged in) and a consent screen listing what the app is requesting. The user approves.
  4. Authorization code returned. Google redirects back to your redirect_uri with a short-lived authorization code:
    https://yourapp.com/callback?code=AUTH_CODE_HERE&state=RANDOM_STATE_VALUE
  5. Verify state parameter. Your server checks that the returned state matches what was stored in the session. This prevents CSRF attacks.
  6. Exchange code for tokens (server-to-server). Your server makes a POST request to Google's token endpoint. The browser is not involved in this step, so the tokens are never exposed to JavaScript:
    POST https://oauth2.googleapis.com/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=AUTH_CODE_HERE
    &redirect_uri=https://yourapp.com/callback
    &client_id=YOUR_CLIENT_ID
    &client_secret=YOUR_CLIENT_SECRET
  7. Receive access token and refresh token. Google responds with:
    {
      "access_token": "ya29.a0AfH6SMBx...",
      "expires_in": 3600,
      "refresh_token": "1//0dZ8nFhq...",
      "scope": "openid email profile https://www.googleapis.com/auth/calendar.readonly",
      "token_type": "Bearer",
      "id_token": "eyJhbGciOiJSUzI1NiIs..."
    }
  8. Use the access token. Include it as a Bearer token in API requests:
    GET https://www.googleapis.com/calendar/v3/calendars/primary/events
    Authorization: Bearer ya29.a0AfH6SMBx...

PKCE: Authorization Code Flow for Public Clients

Single-page apps (SPAs) and mobile apps cannot securely store a client_secret - it would be visible in JavaScript source code or extracted from the app binary. PKCE (Proof Key for Code Exchange, pronounced "pixie") solves this without a client secret.

// 1. Generate a random code_verifier (43-128 characters)
const codeVerifier = generateSecureRandom(64); // e.g., "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_..."

// 2. Compute code_challenge = BASE64URL(SHA256(code_verifier))
const codeChallenge = base64url(sha256(codeVerifier));

// 3. Include code_challenge in the authorization request
// https://auth.example.com/authorize?
//   response_type=code
//   &client_id=app123
//   &code_challenge=COMPUTED_CHALLENGE
//   &code_challenge_method=S256
//   &redirect_uri=https://app.example.com/callback

// 4. When exchanging code for token, include code_verifier
// POST /token
// grant_type=authorization_code
// &code=AUTH_CODE
// &code_verifier=ORIGINAL_VERIFIER  (server verifies SHA256(verifier) == challenge)
// &client_id=app123

The authorization server stores the code_challenge when the authorization request is made. When the code is exchanged, the server hashes the submitted code_verifier and checks it matches the stored challenge. An attacker who intercepts the authorization code cannot exchange it without the original code_verifier.

As of 2024, PKCE is recommended for ALL new OAuth clients, including server side web apps - not just SPAs and mobile apps. It provides an additional layer of protection against authorization code interception attacks. The client secret and PKCE can be used together.

Access Tokens and Refresh Tokens

Access Tokens

Access tokens are credentials that grant access to protected resources. In most modern implementations they are JWTs (JSON Web Tokens) - self-contained tokens that encode the user's identity, scopes, and expiry in a base64-encoded payload signed by the authorization server.

// A JWT access token has three parts: header.payload.signature
// Decode the payload (middle part) to see the claims:
{
  "sub": "user123",           // subject: user ID
  "iss": "https://auth.example.com",  // issuer
  "aud": "api.example.com",   // audience: which API this token is for
  "exp": 1743000000,          // expiry (Unix timestamp)
  "iat": 1742996400,          // issued at
  "scope": "read:profile write:calendar",
  "email": "user@example.com"
}

Inspect any JWT with our free JWT Decoder to see its claims, verify its structure, and check expiry without installing anything.

Access tokens are typically short-lived: 1 hour is common. When they expire, the API returns a 401 Unauthorized response.

Refresh Tokens

Refresh tokens allow the client to obtain new access tokens without asking the user to log in again. They are long-lived (days, weeks, or indefinitely until revoked). The client sends the refresh token to the token endpoint to get a fresh access token:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=1//0dZ8nFhq...
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Important security considerations for refresh tokens:

  • Store refresh tokens server side, never in localStorage (vulnerable to XSS). Use HttpOnly cookies.
  • Implement refresh token rotation: issue a new refresh token with every refresh, invalidate the old one. If a stolen refresh token is used, the legitimate user's next refresh will fail, alerting you to the compromise.
  • Set absolute expiry on refresh tokens. Do not issue tokens that never expire.

Scopes: Limiting Access

Scopes define the specific permissions being requested. They are strings defined by the resource server. Common examples:

# Google API scopes
openid                                       # basic identity (OpenID Connect)
email                                        # user's email address
profile                                      # name, picture
https://www.googleapis.com/auth/calendar.readonly  # read-only calendar
https://www.googleapis.com/auth/gmail.send   # send email only

# GitHub scopes
repo                # full repository access
read:user           # read user profile
gist                # create gists

# Custom API scopes (your own API)
read:orders         # read orders
write:orders        # create/update orders
admin:users         # manage users

Always request the minimum scopes needed. Users are more likely to grant access when the permissions requested are narrow and clearly justified. An app that requests gmail.readonly when it only needs calendar.readonly will raise user suspicion and reduce conversion.

Grant Types: Which Flow to Use

Authorization Code + PKCE

Use for: web apps (server side or SPA), mobile apps, desktop apps. This is the correct choice for any application where a human user is logging in. Always use PKCE.

Client Credentials

Use for: machine-to-machine (M2M) communication. No user is involved. A backend service authenticates directly to another service using its client_id and client_secret. Example: your cron job calling an internal API, a microservice authenticating to another microservice.

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=SERVICE_CLIENT_ID
&client_secret=SERVICE_CLIENT_SECRET
&scope=read:metrics write:events

Device Authorization Grant

Use for: devices with limited input capability (smart TVs, CLI tools, IoT devices). The device displays a code and a URL. The user visits the URL on their phone/computer and enters the code. The device polls the token endpoint until the user approves.

Implicit Flow (Deprecated)

The implicit flow returned access tokens directly in the URL fragment, avoiding the code-for-token exchange. It was designed for SPAs before CORS was widely supported. It is now deprecated in OAuth 2.1 because tokens in URL fragments can be leaked via browser history, referrer headers, and JavaScript access. Use Authorization Code + PKCE instead.

Resource Owner Password Credentials (Deprecated)

The client collects the user's username and password and sends them to the authorization server. This defeats the entire purpose of OAuth - the client sees the credentials. It is deprecated in OAuth 2.1. The only legitimate use case was migrating legacy systems to OAuth, and that transition should be complete by now.

Decode and Inspect JWT Tokens Instantly

Paste any JWT access token and see its decoded header, payload claims, and signature. Free, entirely client side - your tokens never leave your browser.

Open JWT Decoder

OAuth 2.0 vs OpenID Connect

OAuth 2.0 is an authorization framework - it answers "what can this application do?" OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0 - it answers "who is this user?"

OIDC adds:

  • The openid scope, which signals an OIDC request
  • An ID token (a JWT containing user identity claims: sub, email, name, etc.)
  • A UserInfo endpoint where the client can fetch additional user claims using the access token
  • Standardized metadata discovery via /.well-known/openid-configuration

When you implement "Sign in with Google," you are using OIDC on top of OAuth 2.0. The ID token tells you who the user is; the access token lets you call Google APIs on their behalf. Most identity providers (Auth0, Okta, Keycloak, Cognito) implement OIDC.

Common OAuth 2.0 Security Mistakes

  • Not validating the state parameter: The state parameter in the authorization request must be verified on the callback. It is the primary CSRF protection in OAuth. Omitting this check allows an attacker to trick a user into authorizing the attacker's session.
  • Storing tokens in localStorage: localStorage is accessible to any JavaScript on the page, including injected XSS. Store access tokens in memory (JS variable) and refresh tokens in HttpOnly cookies. Use short-lived access tokens to limit exposure.
  • Not using HTTPS for redirect URIs: The authorization code or token in a redirect URI is intercepted in plaintext if the redirect target uses HTTP. Always register HTTPS redirect URIs in production.
  • Overly broad token scopes: Requesting more permissions than needed increases the blast radius if a token is compromised. Always request the minimum necessary scopes.
  • Not implementing refresh token rotation: Static refresh tokens that never expire become long-term credentials. Rotation limits the damage window if a token is stolen.
  • Not validating ID token claims: When using OIDC, always validate the ID token: check iss matches the expected issuer, aud matches your client ID, and exp has not passed. Use a well-maintained OIDC library rather than manual validation.

Frequently Asked Questions

What is the difference between authentication and authorization?

Authentication answers "who are you?" - it verifies identity. Authorization answers "what are you allowed to do?" - it controls access. OAuth 2.0 is an authorization framework. It was designed for API access delegation, not for login. OpenID Connect (OIDC) adds authentication on top of OAuth 2.0. When you implement "Sign in with Google," you use OIDC for authentication and optionally OAuth for API access. Many developers confuse the two; use the right protocol for each job.

How long should access tokens be valid?

One hour is a common default and a reasonable choice. Shorter tokens (15 minutes) reduce the window of exposure if a token is compromised but require more frequent refresh operations. Longer tokens (24 hours) simplify implementation but mean a compromised token remains valid longer. The right answer depends on your threat model. For high-security applications (banking, healthcare), use short-lived tokens with refresh. For internal tools with a low attack surface, longer-lived tokens may be acceptable.

Can OAuth tokens be revoked?

Refresh tokens can be revoked at the authorization server by calling the revocation endpoint (RFC 7009). The authorization server marks the token invalid and subsequent refresh requests with that token will fail. Access tokens are trickier because they are often validated locally (by checking the JWT signature and claims without a network call). This means a revoked access token remains valid until it expires naturally. For applications that need immediate revocation, use short-lived access tokens and implement a token introspection endpoint that the resource server calls for every request, or use a token blocklist cache.

What is the difference between OAuth 2.0 and OAuth 2.1?

OAuth 2.1 is a consolidating update to OAuth 2.0 (still in draft as of 2026) that removes deprecated and insecure features, makes PKCE mandatory for all authorization code flows, deprecates the implicit flow and resource owner password credentials flow, tightens redirect URI matching requirements, and recommends refresh token rotation. It does not introduce new flows - it removes the unsafe ones and documents the current best practices. If you implement OAuth 2.0 today following current best practices (PKCE, no implicit flow, refresh token rotation), you are essentially implementing OAuth 2.1 already.

Should I build my own OAuth server or use an existing one?

In almost all cases: use an existing one. Building a correct, secure OAuth 2.0 authorization server is significantly complex. Use Auth0, Okta, AWS Cognito, Keycloak, or Authentik. These handle all the security edge cases, provide a consent UI, support social login providers, implement token rotation, offer audit logs, and integrate with OIDC. The time you would spend building and maintaining a correct OAuth server is almost never justified. The exception is if you are building an identity provider as a product, have extremely strict data residency requirements, or need complete control over the protocol implementation.

How do I debug OAuth flows when things go wrong?

Start with the error response: OAuth 2.0 defines standardized error codes (invalid_client, invalid_grant, invalid_scope, access_denied) that tell you exactly what failed. Decode the tokens involved with our JWT Decoder to check claims and expiry. Use browser DevTools Network tab to inspect the redirect chain and parameter values. Use your authorization server's event logs - most providers (Auth0, Okta, Cognito) have detailed event logs for every authorization attempt. Common issues: mismatched redirect URI (must match exactly, including trailing slashes), expired authorization code (codes expire in 60 seconds), wrong scope (requesting a scope the client is not authorized for), and clock skew (JWT exp checks fail if client and server clocks diverge by more than a few minutes).

The Bottom Line

OAuth 2.0 is the standard for API authorization for good reason: it solves the credential sharing problem elegantly, provides a flexible framework for different use cases, and has a rich ecosystem of libraries and identity providers. For most applications, the implementation path is:

  1. Use an established identity provider (Auth0, Cognito, etc.) rather than building your own
  2. Use the Authorization Code Flow with PKCE for all user-facing applications
  3. Use Client Credentials for machine-to-machine
  4. Store refresh tokens in HttpOnly cookies, access tokens in memory
  5. Implement refresh token rotation
  6. Request only the minimum scopes you need
  7. Always validate the state parameter

Use our free tool here → JWT Decoder to inspect access tokens and ID tokens in your OAuth flows.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.