OAuth 2.0 is the industry-standard authorization framework that underpins nearly every modern API, mobile app, and single-page application. Yet even experienced developers struggle with choosing the right flow, securing tokens, and understanding where OAuth ends and OpenID Connect begins. This guide consolidates everything you need to know about OAuth 2.0 into a single reference, with links to deep-dive articles for each topic.
Whether you are building a React SPA, a microservice mesh, or a mobile application, by the end of this guide you will understand how every piece of the OAuth ecosystem fits together and which patterns to apply in your specific architecture.
What Is OAuth 2.0
OAuth 2.0 (RFC 6749) is a delegation protocol that allows a user to grant a third-party application limited access to a resource without sharing their credentials. It replaced OAuth 1.0’s complex signature mechanism with bearer tokens transmitted over TLS.
The Four Actors
Every OAuth 2.0 interaction involves four roles:
| Actor | Description | Example |
|---|---|---|
| Resource Owner | The entity that owns the data | End user |
| Client | The application requesting access | React SPA, mobile app |
| Authorization Server | Issues tokens after authenticating the resource owner | Keycloak, Auth0, Okta |
| Resource Server | Hosts the protected API | Your backend REST API |
Grant Types at a Glance
OAuth 2.0 defines several grant types (also called “flows”). Each serves a different architecture:
- Authorization Code – The most secure flow for user-facing apps. The client receives an authorization code via a browser redirect and exchanges it for tokens at the token endpoint. For a step-by-step walkthrough, see Understanding Authorization Code Flow.
- Authorization Code with PKCE – Extends the authorization code flow with a cryptographic proof, required for public clients. See Authorization Code Flow with PKCE.
- Client Credentials – For machine-to-machine communication where no user is involved.
- Refresh Token – Not a standalone flow but a mechanism to obtain new access tokens silently.
- Implicit (deprecated) – Previously used for SPAs; replaced by Authorization Code with PKCE.
- Resource Owner Password Credentials (deprecated) – Anti-pattern that exposes user credentials directly to the client.
For a comparison of the two most common flows, see our article on OAuth 2.0 Best Practices.
OAuth 2.0 vs OpenID Connect
One of the most common sources of confusion is the relationship between OAuth 2.0 and OpenID Connect (OIDC). They are complementary, not competing, protocols.
OAuth 2.0 answers the question: “What is this application allowed to do?” It is an authorization framework. It issues access tokens that grant permission to call APIs, but it says nothing about who the user is.
OpenID Connect answers the question: “Who is this user?” It is an authentication layer built on top of OAuth 2.0. It adds:
- An ID token (a JWT containing identity claims like
sub,email,name) - A UserInfo endpoint for fetching additional profile data
- A Discovery document (
.well-known/openid-configuration) for automatic client configuration - Standard scopes (
openid,profile,email,address,phone)
When to use which:
- If you only need to call an API on behalf of a user (e.g., read their calendar), OAuth 2.0 is sufficient.
- If you need to log the user in and know their identity, use OpenID Connect.
- If you need both, use OIDC – it automatically includes OAuth 2.0 capabilities.
For a detailed comparison, read OAuth 2.0 vs OIDC. For a broader protocol comparison that includes SAML, see SAML vs OIDC and SAML, OIDC, OAuth Deep Dive.
Authorization Code Flow with PKCE
The Authorization Code Flow with PKCE (Proof Key for Code Exchange, pronounced “pixy”) is the recommended flow for all user-facing applications in 2026. OAuth 2.1 mandates PKCE for every client type.
How It Works
- The client generates a random
code_verifier(43-128 characters) and derives acode_challengeusing SHA-256. - The client sends the user to the authorization endpoint with the
code_challenge. - The user authenticates and consents.
- The authorization server redirects back with an
authorization_code. - The client exchanges the code at the token endpoint, including the original
code_verifier. - The authorization server verifies
SHA256(code_verifier) == code_challengebefore issuing tokens.
Authorization Request
GET /authorize?
response_type=code
&client_id=my-spa
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=abc123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
HTTP/1.1
Host: auth.example.com
Token Exchange
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
&client_id=my-spa
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Token Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"scope": "openid profile email"
}
Why PKCE Matters
Without PKCE, an attacker who intercepts the authorization code (through a malicious browser extension, a compromised redirect URI, or an OS-level custom scheme handler on mobile) can exchange it for tokens. PKCE ensures only the client that initiated the request can complete the exchange.
For implementation details, see How PKCE Enhances Security and try our interactive PKCE Generator Tool.
Client Credentials Flow
The Client Credentials Flow is designed for server-to-server communication where no user is involved. The client authenticates directly with the authorization server using its own credentials (client ID and client secret) and receives an access token.
When to Use
- Microservice-to-microservice calls
- Batch jobs and cron tasks
- Backend services accessing third-party APIs
- CI/CD pipelines that need API access
Token Request
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic bXlDbGllbnRJZDpteUNsaWVudFNlY3JldA==
grant_type=client_credentials
&scope=api:read api:write
Token Response
{
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhdXRoLmV4YW1wbGUuY29tIiwic3ViIjoibXlDbGllbnRJZCIsImF1ZCI6ImFwaS5leGFtcGxlLmNvbSIsImV4cCI6MTcwNzg5MjAwMCwic2NvcGUiOiJhcGk6cmVhZCBhcGk6d3JpdGUifQ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}
Note that no refresh_token or id_token is returned – there is no user to refresh on behalf of, and no identity to assert.
Security Considerations
- Never expose client secrets in front-end code, mobile apps, or public repositories.
- Rotate secrets regularly and use short-lived access tokens.
- Limit scopes to the minimum required for the service.
- Consider mTLS client authentication (RFC 8705) for high-security environments instead of shared secrets.
For a comprehensive walkthrough, read Client Credentials Flow.
Refresh Token Management
Access tokens are intentionally short-lived (typically 5-60 minutes). Refresh tokens allow clients to obtain new access tokens without forcing the user to re-authenticate.
The Refresh Flow
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=my-spa
The authorization server responds with a new access token (and optionally a new refresh token):
{
"access_token": "eyJhbGciOiJSUzI1NiJ9.new-payload...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpc0lzQU5ld1JlZnJlc2hUb2tlbg"
}
Refresh Token Rotation
For public clients (SPAs, mobile apps) that cannot keep a client secret, refresh token rotation is a critical security measure. With rotation:
- Each time the client uses a refresh token, the authorization server issues a new refresh token and invalidates the old one.
- If an attacker steals and uses the old refresh token, the authorization server detects the reuse and revokes the entire token family.
- The legitimate user is forced to re-authenticate, but the attacker is locked out.
Best Practices
- Always use rotation for public clients.
- Set absolute lifetime limits on refresh tokens (e.g., 7-30 days).
- Implement idle timeout – revoke if unused for a period.
- Store securely: HttpOnly cookies for web apps, secure storage for mobile.
- Revoke on logout – call the revocation endpoint when the user signs out.
For implementation examples in Java, see Refresh Tokens in OAuth 2.0.
JWT Tokens: Structure, Validation, and Security
JSON Web Tokens (JWTs) are the most common format for OAuth 2.0 access tokens and OIDC ID tokens. Understanding their structure is essential for secure token handling.
JWT Structure
A JWT consists of three Base64URL-encoded parts separated by dots:
Header – specifies the algorithm and token type:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-02"
}
Payload – contains the claims:
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"aud": "https://api.example.com",
"exp": 1707892000,
"iat": 1707888400,
"scope": "openid profile email",
"email": "[email protected]",
"name": "Jane Developer"
}
Signature – created by signing the header and payload with the authorization server’s private key:
Token Validation Checklist
Every resource server that receives a JWT must validate it before trusting the claims. Here is the minimum validation:
- Decode the token and parse the header, payload, and signature.
- Verify the signature using the authorization server’s public key (fetched from the JWKS endpoint).
- Check
iss(issuer) – must match your expected authorization server. - Check
aud(audience) – must include your API’s identifier. - Check
exp(expiration) – reject if the token is expired. Allow a small clock skew (30-60 seconds). - Check
iat(issued at) – optionally reject tokens issued too far in the past. - Check
nbf(not before) – reject if the token is not yet valid. - Validate scopes – ensure the token grants the permissions required for the requested operation.
import jwt
import requests
# Fetch the JWKS from the authorization server
jwks_url = "https://auth.example.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()
# Decode and validate the token
try:
payload = jwt.decode(
token,
jwks, # Public keys
algorithms=["RS256"], # Expected algorithm
audience="https://api.example.com",
issuer="https://auth.example.com",
options={"require": ["exp", "iss", "aud", "sub"]}
)
print(f"Authenticated user: {payload['sub']}")
except jwt.ExpiredSignatureError:
print("Token has expired")
except jwt.InvalidAudienceError:
print("Token audience mismatch")
except jwt.InvalidIssuerError:
print("Token issuer mismatch")
Common JWT Pitfalls
- Never trust the
algheader blindly. Always enforce the expected algorithm on the server side to prevent algorithm confusion attacks. - Never use
"alg": "none". This disables signature verification entirely. - Never store JWTs in localStorage. They become vulnerable to XSS. Use HttpOnly cookies instead.
- Never put sensitive data in the payload. JWTs are Base64-encoded, not encrypted. Anyone can read the claims.
For a beginner-friendly introduction, read What is a JWT. For production validation patterns, see JWT Decoding and Validation. You can also inspect tokens interactively with our JWT Decode Tool and construct test tokens with the JWT Builder Tool.
OAuth for Single-Page Applications
SPAs present unique security challenges because they run entirely in the browser – there is no server-side component to store secrets or proxy token requests. The OAuth community has converged on two patterns.
Pattern 1: Authorization Code with PKCE (Direct)
The SPA performs the OAuth flow directly with the authorization server using PKCE. Tokens are stored in memory (not localStorage) and refreshed using refresh token rotation with short-lived refresh tokens.
Pros:
- Simpler architecture, no backend proxy needed
- Works well for APIs on the same domain
Cons:
- Tokens are exposed to JavaScript (XSS risk)
- Refresh tokens in the browser require strict rotation and short lifetimes
- Cross-origin issues with third-party cookies being blocked
For an implementation guide, see PKCE in SPAs.
Pattern 2: Backend-for-Frontend (BFF)
The BFF pattern introduces a thin backend proxy that handles the OAuth flow on behalf of the SPA. The proxy stores tokens server-side and issues a session cookie to the SPA.
Pros:
- Tokens never reach the browser – immune to XSS token theft
- Works with third-party cookie restrictions
- Supports confidential clients (client secret on the server)
Cons:
- Requires a backend service
- Adds latency from the extra hop
The BFF pattern is recommended by the OAuth Working Group for SPAs that need high security. For a React implementation, see OAuth BFF for React SPA.
Which Pattern to Choose
| Criteria | PKCE Direct | BFF |
|---|---|---|
| Token exposure to JS | Yes | No |
| Requires backend | No | Yes |
| Third-party cookie issues | Possible | None |
| Suitable for high-security apps | With caveats | Yes |
| Implementation complexity | Lower | Higher |
For most production applications handling sensitive data, the BFF pattern is the safer choice.
OAuth 2.1: What Is Changing
OAuth 2.1 (draft RFC) consolidates best practices from years of OAuth 2.0 usage into the core specification. It does not introduce new concepts; rather, it makes existing security recommendations mandatory.
Key Changes from OAuth 2.0
- PKCE is mandatory for all authorization code grants, including confidential clients.
- Implicit grant is removed. SPAs must use Authorization Code with PKCE.
- Resource Owner Password Credentials (ROPC) grant is removed. Applications must not collect user passwords.
- Refresh tokens must be sender-constrained or use rotation for public clients.
- Redirect URIs must use exact string matching. Wildcard and pattern matching are no longer allowed.
- Bearer tokens in query strings are prohibited. Tokens must be sent in the
Authorizationheader or POST body.
Migration Checklist
If you are currently running OAuth 2.0, here is what to update:
- Add PKCE to all authorization code flows (even confidential clients)
- Remove any Implicit grant configurations
- Remove any ROPC grant configurations
- Enable refresh token rotation for public clients
- Audit redirect URIs for exact matching
- Ensure tokens are not passed in query parameters
- Update client libraries to the latest versions
Example: Adding PKCE to a Confidential Client
Even if your server-side application already uses a client secret, OAuth 2.1 requires PKCE. Here is a Node.js example:
const crypto = require('crypto');
// Generate PKCE values
function generatePKCE() {
const verifier = crypto.randomBytes(32)
.toString('base64url');
const challenge = crypto.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
const { verifier, challenge } = generatePKCE();
// Include in authorization request
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', 'my-confidential-app');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex'));
// Store verifier in session for token exchange
session.pkceVerifier = verifier;
For the full OAuth 2.1 breakdown, read OAuth 2.1 Complete Guide.
Security Best Practices
OAuth security is not just about choosing the right flow. The details of your implementation determine whether your system is truly secure. Here are the practices that matter most.
Use State to Prevent CSRF
The state parameter ties the authorization request to the user’s browser session. Without it, an attacker could craft a malicious authorization URL and trick the user into linking the attacker’s account.
// Generate state and store in session
const state = crypto.randomBytes(16).toString('hex');
session.oauthState = state;
// Verify on callback
if (req.query.state !== session.oauthState) {
throw new Error('State mismatch -- possible CSRF attack');
}
Use Nonce to Prevent Replay Attacks
The nonce parameter (used with OIDC) prevents ID token replay attacks. The authorization server includes the nonce in the ID token, and the client verifies it matches the value sent in the original request.
// Include nonce in authorization request
const nonce = crypto.randomBytes(16).toString('hex');
session.oidcNonce = nonce;
// After receiving the ID token, verify
const decoded = jwt.decode(idToken);
if (decoded.nonce !== session.oidcNonce) {
throw new Error('Nonce mismatch -- possible replay attack');
}
Combine State, Nonce, and PKCE
For maximum security, use all three parameters together:
| Parameter | Protects Against | Used In |
|---|---|---|
state | CSRF attacks | OAuth 2.0, OIDC |
nonce | ID token replay | OIDC only |
PKCE | Authorization code interception | OAuth 2.0, OIDC |
For a deep dive into these three mechanisms and how they differ, see State vs Nonce vs PKCE.
Token Storage
How you store tokens is as important as how you obtain them:
- Server-side apps: Store tokens in an encrypted server-side session. Never expose them to the browser.
- SPAs (PKCE direct): Store access tokens in memory (JavaScript variable). Use refresh token rotation with strict lifetimes. Never use localStorage.
- SPAs (BFF pattern): Tokens stay on the server. The browser only receives an HttpOnly, Secure, SameSite session cookie.
- Mobile apps: Use platform-specific secure storage (Keychain on iOS, EncryptedSharedPreferences on Android).
Additional Recommendations
- Always use TLS. OAuth tokens are bearer tokens – anyone who intercepts them gains access.
- Validate redirect URIs exactly. Register the full URI including path; reject any deviations.
- Implement token revocation. Call the revocation endpoint on logout (RFC 7009).
- Use short-lived access tokens. 5-15 minutes is a good range for most applications.
- Monitor for anomalies. Log token usage patterns and alert on unusual activity (geographic anomalies, excessive token refreshes).
For a broader overview of production-grade patterns, see OAuth 2.0 Best Practices and OAuth 2.0 Deep Dive.
Tools and Libraries
Interactive Developer Tools
Building and debugging OAuth flows is easier with the right tools. IAMDevBox provides several interactive utilities:
- JWT Decode Tool – Paste any JWT to decode its header, payload, and verify its structure. Essential for debugging token issues.
- JWT Builder Tool – Construct JWTs with custom claims for testing your resource server validation logic.
- PKCE Generator Tool – Generate code_verifier and code_challenge pairs for testing PKCE flows.
- OIDC Discovery Checker – Inspect any OpenID Connect provider’s discovery document and JWKS endpoint.
Recommended Libraries
When implementing OAuth, always use a well-maintained library rather than building from scratch:
JavaScript / Node.js:
openid-client– Full-featured OIDC Relying Party libraryjose– JWT/JWS/JWE/JWK library with no dependenciespassportwithpassport-openidconnect– Express middleware
Java / Spring:
- Spring Security OAuth 2.0 Client – Built-in support in Spring Boot
- Nimbus JOSE + JWT – Low-level JWT library
- Keycloak Java Adapter – For Keycloak deployments
Python:
authlib– Comprehensive OAuth/OIDC libraryPyJWT– JWT encoding and decodingoauthlib– Generic OAuth library used byrequests-oauthlib
Go:
golang.org/x/oauth2– Standard OAuth 2.0 packagecoreos/go-oidc– OIDC client librarylestrrat-go/jwx– JWT/JWK library
Authorization Server Products
If you need to run your own authorization server, here are the most common options in the IAM ecosystem:
- Keycloak – Open-source, feature-rich, supports OIDC, SAML, and OAuth 2.0 out of the box
- ForgeRock Access Management – Enterprise-grade IAM with advanced policy management
- PingFederate – Ping Identity’s federation server with broad protocol support
- Auth0 – Developer-friendly identity platform (now part of Okta)
- Okta – Workforce and customer identity with extensive API support
Choosing the Right Flow: Decision Tree
Not sure which OAuth flow fits your application? Use this decision tree:
For context on how this maps to specific grant types and their trade-offs, see Understanding Authorization Code Flow and Client Credentials Flow.
Putting It All Together
OAuth 2.0 is not a single specification you implement once and forget. It is an ecosystem of interrelated specifications, extensions, and best practices that evolve over time. Here is how the pieces connect:
- OAuth 2.0 provides the core authorization framework and grant types.
- OpenID Connect adds user authentication on top of OAuth 2.0.
- PKCE secures the authorization code flow against interception attacks.
- JWTs provide a self-contained, verifiable token format.
- Refresh tokens enable long-lived sessions without compromising security.
- OAuth 2.1 codifies all of the above into a single, streamlined specification.
The key to a secure implementation is understanding which pieces apply to your architecture, using well-tested libraries, and following the principle of least privilege at every layer.
Further Reading
Explore these articles for deeper coverage of specific topics:
- Understanding Authorization Code Flow – The foundational flow explained
- Authorization Code Flow with PKCE – Step-by-step PKCE implementation
- PKCE in SPAs – SPA-specific implementation patterns
- Client Credentials Flow – Machine-to-machine authorization
- Refresh Tokens in OAuth 2.0 – Token lifecycle with Java examples
- OAuth 2.1 Complete Guide – Everything changing in OAuth 2.1
- OAuth 2.0 vs OIDC – Protocol comparison
- State vs Nonce vs PKCE – Security parameter deep dive
- OAuth BFF for React SPA – BFF pattern implementation
- JWT Decoding and Validation – Production JWT patterns
- What is a JWT – JWT fundamentals
- SAML vs OIDC – When to use which federation protocol

