# OAuth 2.0 Flows Explained: Authorization Code, PKCE, and More

> Understand OAuth 2.0 grant types, when to use each flow, how PKCE protects public clients, and security best practices for token-based authentication.

- URL: https://www.browserutils.dev/blog/oauth2-flows-explained
- Published: 2026-09-07
- Updated: 2026-07-02

---

OAuth 2.0 is the authorization framework behind "Sign in with Google," API access tokens, and most of the delegated authorization you encounter on the web. It lets a user grant a third-party application access to their resources without sharing their password. (See our [OAuth glossary entry](/glossary/oauth) for a quick-reference definition.)

But OAuth 2.0 is not a single protocol — it is a family of flows (called grant types), each designed for different scenarios. Choosing the wrong flow introduces security vulnerabilities. This guide explains each grant type, when to use it, and the security considerations that matter.

## Core Concepts

Before diving into the flows, you need to understand the roles involved:

- **Resource Owner**: The user who owns the data (e.g., you and your GitHub repos)
- **Client**: The application requesting access (e.g., a CI/CD tool that needs to read your repos)
- **Authorization Server**: The server that authenticates the user and issues tokens (e.g., GitHub's OAuth server)
- **Resource Server**: The API that holds the protected data (e.g., GitHub's API)

OAuth 2.0 issues two types of tokens:

- **Access Token**: Short-lived token used to access the API. Typically expires in minutes to hours.
- **Refresh Token**: Longer-lived token used to obtain new access tokens without re-authenticating the user.

Access tokens are often JWTs (JSON Web Tokens), which encode claims about the user and permissions. You can inspect the contents of a JWT using our [JWT decoder](/tools/jwt-decoder) to verify what scopes and claims are included.

## Authorization Code Flow

This is the most secure and most commonly used flow for server-side web applications. It keeps tokens off the browser's URL bar and handles the exchange server-side.

### How It Works

1. Your application redirects the user to the authorization server with a request for specific scopes
2. The user authenticates and approves the requested permissions
3. The authorization server redirects back to your application with a **authorization code**
4. Your server exchanges the code for an access token (and optionally a refresh token) using a server-to-server request that includes your client secret

```
# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
  response_type=code&
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  scope=read:profile&
  state=random_csrf_token

# Step 3: Authorization server redirects back
GET https://yourapp.com/callback?
  code=AUTHORIZATION_CODE&
  state=random_csrf_token

# Step 4: Exchange code for token (server-to-server)
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=your_client_id&
client_secret=your_client_secret
```

### Why the Extra Step?

The authorization code is a one-time-use intermediary. The actual access token is never exposed in the browser's URL or history. The exchange happens server-to-server, where the client secret can be kept safe.

### When to Use It

- Server-rendered web applications (Node.js, Django, Rails, etc.)
- Any application with a backend that can securely store a client secret

## Authorization Code Flow with PKCE

PKCE (Proof Key for Code Exchange, pronounced "pixy") solves a critical problem: what happens when your client cannot keep a secret? Single-page applications, mobile apps, and desktop applications cannot safely embed a client secret because the code is accessible to the user.

### How It Works

PKCE adds a dynamic secret to each authorization request:

1. Your application generates a random **code verifier** (a high-entropy string)
2. It creates a **code challenge** by hashing the verifier with SHA-256 and base64url-encoding the result
3. The code challenge is sent with the initial authorization request
4. When exchanging the authorization code for tokens, the original code verifier is sent instead of a client secret
5. The authorization server hashes the verifier and compares it to the stored challenge

```
# Generate PKCE values
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = base64url(sha256(code_verifier))
# Result: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

# Step 1: Include challenge in authorization request
GET https://auth.example.com/authorize?
  response_type=code&
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  scope=read:profile&
  state=random_csrf_token&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256

# Step 4: Include verifier in token exchange
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=your_client_id&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
```

Even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the code verifier. The verifier was generated in your app and never sent through the redirect URL.

The code challenge is essentially a base64-encoded hash. You can experiment with base64 encoding using our [Base64 encoder](/tools/base64-encode-decode) to understand the encoding step.

### When to Use It

- Single-page applications (React, Vue, Angular)
- Mobile applications (iOS, Android)
- Desktop applications
- **Any public client** — PKCE is now recommended for all OAuth clients, even confidential ones

## Client Credentials Flow

This flow is for machine-to-machine communication where no user is involved. Your application authenticates as itself, not on behalf of a user.

```
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&
client_id=your_client_id&
client_secret=your_client_secret&
scope=read:analytics
```

The server returns an access token directly. No redirects, no user interaction.

### When to Use It

- Backend services calling other backend services
- Scheduled jobs and cron tasks
- Microservice-to-microservice communication
- CLI tools that act on behalf of the application, not a user

## Device Authorization Flow

This flow is designed for devices with limited input capabilities — smart TVs, IoT devices, CLI tools — where typing a URL or entering credentials is impractical.

1. The device requests a device code and a user code from the authorization server
2. The device displays the user code and a URL to the user
3. The user visits the URL on their phone or computer and enters the code
4. The device polls the authorization server until the user completes authentication
5. The server returns an access token

You have seen this flow when connecting a streaming app on your TV — it shows a code like "ABCD-1234" and asks you to visit a URL.

### When to Use It

- Smart TVs and set-top boxes
- IoT devices
- CLI tools where browser-based login is preferred

## Implicit Flow (Deprecated)

The implicit flow returned the access token directly in the URL fragment after authorization. It was designed for SPAs before PKCE existed.

```
# Token returned in URL fragment (visible in browser)
https://yourapp.com/callback#access_token=TOKEN&token_type=bearer&expires_in=3600
```

**Do not use this flow.** It exposes the access token in the URL, browser history, and referrer headers. The authorization code flow with PKCE is the replacement for all cases where the implicit flow was previously used.

## Resource Owner Password Credentials (Deprecated)

This flow sends the user's username and password directly to the token endpoint:

```
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&
username=user@example.com&
password=their_password&
client_id=your_client_id
```

**Do not use this flow.** It requires the application to handle the user's password, which defeats the purpose of OAuth. It exists only for migrating legacy applications.

## Choosing the Right Flow

| Scenario | Recommended Flow |
|---|---|
| Server-side web app | Authorization Code |
| Single-page application | Authorization Code + PKCE |
| Mobile app | Authorization Code + PKCE |
| Desktop app | Authorization Code + PKCE |
| Backend service / API | Client Credentials |
| Smart TV / IoT | Device Authorization |
| CLI tool | Device Authorization or Client Credentials |

## Security Best Practices

### Token Storage

- **Server-side apps**: Store tokens in an encrypted session store. Never expose them to the browser.
- **SPAs**: Use in-memory storage (JavaScript variables). Avoid localStorage — it is accessible to any script on the page, including XSS payloads.
- **Mobile apps**: Use platform secure storage (Keychain on iOS, Keystore on Android).

### Token Lifetime

- Keep access tokens short-lived (5-60 minutes)
- Use refresh tokens for long-lived sessions
- Implement refresh token rotation — issue a new refresh token with each use and invalidate the old one

### Scope Minimization

Request only the scopes your application actually needs. `read:profile` is better than `admin:all`. Broad scopes increase the damage from a token leak.

### State Parameter

Always include a `state` parameter in authorization requests and verify it in the callback. This prevents CSRF attacks where an attacker tricks the user into authorizing a request they did not initiate.

```javascript
// Generate state
const state = crypto.randomUUID();
sessionStorage.setItem('oauth_state', state);

// Verify on callback
const returnedState = new URLSearchParams(window.location.search).get('state');
if (returnedState !== sessionStorage.getItem('oauth_state')) {
  throw new Error('CSRF detected: state mismatch');
}
```

### Validate Tokens

Always validate access tokens on the resource server. For JWTs, this means checking the signature, expiration, issuer, and audience claims. Our [JWT decoder](/tools/jwt-decoder) can help you inspect token contents during development, but always validate programmatically in production.

## Common Mistakes

1. **Storing tokens in localStorage**: Vulnerable to XSS. Use httpOnly cookies or in-memory storage.
2. **Not validating the state parameter**: Opens the door to CSRF attacks.
3. **Using the implicit flow**: Replaced by Authorization Code + PKCE. Tokens in URLs are a security risk.
4. **Long-lived access tokens**: If a token is valid for days, a stolen token is useful for days. Keep them short.
5. **Not rotating refresh tokens**: A leaked refresh token without rotation gives indefinite access.
6. **Overly broad scopes**: Request minimum permissions. Revoke scopes you no longer need.

## Summary

OAuth 2.0 gives you the tools for secure delegated authorization, but choosing the wrong flow undermines the entire model. Use Authorization Code with PKCE for anything user-facing, Client Credentials for service-to-service communication, and Device Authorization for constrained devices. Avoid the deprecated implicit and password flows. Keep tokens short-lived, store them securely, and always validate on the server side.