Choosing OAuth Grant Types

The Flows I’ve Actually Shipped

After implementing OAuth flows for SPAs, mobile apps, CLI tools, and server-to-server services, I’ve learned that choosing the wrong grant type creates security vulnerabilities and user experience nightmares. Here’s what actually works in production.

Authorization Code + PKCE (The Default Choice)

This is what I use for nearly everything now: SPAs, mobile apps, native desktop apps, and even traditional server-rendered applications. The flow works like this:

  1. Client generates a cryptographically random code_verifier (43-128 characters)
  2. Client creates code_challenge from the verifier using SHA256
  3. Client redirects to authorization endpoint with the challenge
  4. After user authenticates, auth server returns an authorization code
  5. Client exchanges code for tokens, sending the original code_verifier
  6. Auth server validates that hash(verifier) matches the stored challenge

Why this matters in practice: PKCE (RFC 7636) prevents authorization code interception attacks. I shipped a React SPA in 2023 that initially used plain authorization code flow. During security review, we realized that if an attacker could intercept the redirect (through a compromised proxy or malicious browser extension), they could steal the code and exchange it for tokens. With PKCE, the attacker would also need the code_verifier, which never leaves the client.

The latency cost is negligible—one additional round trip to exchange the code for tokens—but you gain the ability to use refresh tokens safely in public clients. Mobile apps need this for offline access without forcing users to re-authenticate.

Client Credentials (Server-to-Server)

For confidential clients (servers, background jobs, Lambda functions with secure secrets storage), client credentials flow is straightforward: authenticate with client_id and client_secret, receive an access token.

I use this for microservice-to-microservice communication and scheduled jobs. The critical implementation detail: never use this flow from browsers or mobile apps. I once reviewed code where a developer embedded client credentials in a React app to call an API. The secret was visible in the JavaScript bundle. We rotated credentials and migrated to authorization code + PKCE.

Device Authorization Grant (RFC 8628)

This is for input-constrained devices: smart TVs, CLI tools, IoT devices. The flow:

  1. Device requests a device code and user code from the authorization server
  2. Device displays the user code and a URL (e.g., “Visit example.com/activate and enter ABC-DEF”)
  3. User goes to that URL on their phone/computer, enters the code, and authenticates
  4. Device polls the token endpoint until the user completes authentication

I implemented this for a CLI tool that needed to access a user’s cloud resources. The alternative would have been embedding a web server in the CLI to handle redirects, which breaks in environments with firewall restrictions or when users SSH into remote machines. The polling approach works everywhere, though you need to implement exponential backoff to avoid hammering the auth server.

What Not to Use (and Why)

Implicit Flow (Deprecated)

The implicit flow returns access tokens directly in the URL fragment after authentication—no authorization code exchange. This seemed efficient when I first learned OAuth, but it’s fundamentally broken.

Real problem: Tokens appear in browser history, server logs (if fragment isn’t handled correctly), and can leak through Referer headers. I audited an application in 2022 that used implicit flow and found access tokens in both the analytics platform (leaked via Referer) and the CDN logs. OAuth 2.1 officially deprecates this flow.

What developers get wrong: They choose implicit flow because it has fewer steps and they think PKCE is “too complex” for their SPA. This is backwards. Authorization code + PKCE is now simpler to implement correctly using modern libraries (like oidc-client-ts) and is more secure.

Resource Owner Password Credentials (ROPC)

ROPC means the client directly collects the user’s username and password and sends them to the authorization server. This defeats the entire purpose of OAuth: delegated authorization without sharing credentials.

When I used it (and shouldn’t have): In a legacy migration project, we had a mobile app that collected user credentials. Rather than rewriting the authentication UI to use browser-based authorization code flow, we used ROPC “temporarily.” This created technical debt that took months to fix properly. The app couldn’t support SSO, federated identity, or MFA without significant rewrites.

The only legitimate use case: First-party, highly trusted applications where the authorization server itself is collecting credentials in its own UI. Even then, authorization code flow is usually better. OAuth 2.1 recommends against ROPC entirely.

Decision Framework

Here’s the decision tree I use:

Is this a server/confidential client with secure secrets storage?
→ Yes: Client Credentials

Is this an input-constrained device (TV, CLI, IoT)?
→ Yes: Device Authorization Grant

Is this a browser-based SPA, mobile app, or native desktop app?
→ Yes: Authorization Code + PKCE

Do you need offline access (refresh tokens)?
→ Authorization Code + PKCE (request offline_access scope)

Are you considering implicit flow for any reason?
→ No. Use Authorization Code + PKCE instead.

Are you collecting user passwords directly?
→ Stop. Use Authorization Code + PKCE with browser-based login.

Common Mistakes

Mistake 1: Using client credentials from a mobile app because “it’s simpler.” This exposes your client secret to anyone who decompiles the app.

Mistake 2: Choosing implicit flow for SPAs because a 2018 tutorial recommended it. That advice predates the widespread adoption of PKCE for public clients.

Mistake 3: Implementing custom flows instead of using established grant types. I’ve seen developers create “lightweight” authentication by passing credentials in custom headers. This breaks SSO, federation, and token refresh patterns that OAuth handles standardly.

Mistake 4: Not validating redirect URIs strictly. I found an open redirect vulnerability where the auth server accepted any redirect_uri for a client. An attacker could steal authorization codes by manipulating the redirect.

Implementation Checklist

When implementing any OAuth flow:

  • Validate all redirect URIs against an allowlist (RFC 8252 section 7.3 for native apps)
  • Use state parameter to prevent CSRF (RFC 6749 section 10.12)
  • Use PKCE even if your client is confidential—defense in depth
  • Set appropriate token lifetimes: short-lived access tokens (15-60 minutes), long-lived refresh tokens
  • Implement token refresh before expiration to avoid user disruption
  • Store tokens securely: httpOnly cookies for server-rendered apps, secure storage APIs for mobile, memory-only for SPAs

The OAuth specifications (RFC 6749, RFC 7636, RFC 8252) provide the foundation, but implementation details matter. OAuth 2.1 consolidates best practices and deprecates dangerous patterns—follow it as your baseline.

Choose authorization code + PKCE unless you have a specific reason not to. That’s the lesson from shipping OAuth in production.