What Is OAuth 2.0?
OAuth 2.0 is the industry-standard authorization framework that enables third-party applications to obtain limited access to a user's resources without exposing their credentials. Instead of sharing passwords, OAuth 2.0 uses access tokens—short-lived, scoped strings that grant specific permissions to specific resources.
If you've ever clicked "Sign in with Google" or authorized a GitHub app to access your repositories, you've used OAuth 2.0. The protocol was formalized in RFC 6749 and has become the backbone of modern API authorization across the web.
OAuth 2.0 is not an authentication protocol by itself—it is an authorization framework. Authentication (proving who you are) is handled by extensions like OpenID Connect (OIDC), which layers identity on top of OAuth 2.0. Understanding this distinction is critical to building secure systems.
Key Terminology
Before diving into the flows, let's define the four roles that appear in every OAuth 2.0 interaction:
Resource Owner
The entity that owns the protected data—usually the end user. When you authorize Spotify to read your Facebook profile, you are the Resource Owner.
Client
The application requesting access to the Resource Owner's data. Clients are classified as confidential (can securely store a secret, e.g., a backend server) or public (cannot store secrets, e.g., a single-page app or mobile app).
Authorization Server
The server that authenticates the Resource Owner and issues access tokens. Examples include Google's OAuth server, Auth0, Okta, and Keycloak. It exposes two key endpoints: the authorization endpoint (user-facing) and the token endpoint (machine-to-machine).
Resource Server
The API that hosts the protected resources. It validates the access token on each request and returns data if the token is valid and has the required scopes. For instance, the GitHub API is the Resource Server when a third-party app reads your repositories.
Authorization Code Flow (The Gold Standard)
The Authorization Code flow is the most secure and widely recommended OAuth 2.0 grant type for server-side applications. It involves a front-channel redirect to the authorization server and a back-channel token exchange, ensuring the access token never touches the browser.
Step-by-Step Walkthrough
-
User clicks "Login" — The Client redirects the user's browser to the Authorization Server's
/authorizeendpoint withresponse_type=code,client_id,redirect_uri,scope, and a randomstateparameter. - User authenticates — The Authorization Server presents a login screen. The user enters credentials and consents to the requested scopes.
-
Authorization code issued — The Authorization Server redirects back to the Client's
redirect_uriwith a short-lived authorization code and the originalstatevalue. -
Client validates state — The Client checks that the returned
statematches the one it sent. This prevents CSRF attacks. -
Token exchange — The Client's backend sends a POST request to the
/tokenendpoint with the authorization code,client_id,client_secret, andredirect_uri. -
Tokens returned — The Authorization Server responds with an
access_token, arefresh_token(optional),token_type, andexpires_in. -
API call — The Client uses the access token in the
Authorization: Bearerheader to call the Resource Server.
ASCII Flow Diagram
+----------+ +---------------+
| | (1) Authorization Request | |
| User / | -----------------------------> | Authorization |
| Browser | | Server |
| | <----------------------------- | |
| | (2) Login Page + Consent | |
| | | |
| | (3) Authorization Code | |
| | -----------------------------> | |
+----------+ +---------------+
| |
| (3) Redirect with code + state |
v |
+----------+ |
| | (5) Exchange code for token |
| Client | ---------------------------------------->
| (Server) | |
| | <----------------------------------------
| | (6) access_token + refresh_token |
| | +---------------+
| | (7) API Request with token | |
| | -----------------------------> | Resource |
| | | Server |
| | <----------------------------- | |
| | (8) Protected Resource | |
+----------+ +---------------+
The key security property: the authorization code is exchanged server-to-server (step 5), so the access token never appears in the browser's URL bar or history. The code itself is single-use and short-lived (typically 30–60 seconds).
Authorization Code Flow with PKCE
PKCE (Proof Key for Code Exchange, pronounced "pixy") was originally designed for public clients like mobile and single-page apps that cannot safely store a client_secret. However, as of the OAuth 2.0 Security Best Current Practice, PKCE is now recommended for all clients, including confidential ones.
How PKCE Works
- Generate a code verifier — A cryptographically random string (43–128 characters).
-
Create a code challenge — Apply
SHA-256to the verifier and Base64-URL-encode the result. -
Send the challenge — Include
code_challengeandcode_challenge_method=S256in the authorization request. -
Send the verifier — When exchanging the code for a token, include the original
code_verifier. - Server validates — The Authorization Server hashes the verifier and compares it to the stored challenge. If they match, the token is issued.
Client Authorization Server
| |
| (1) /authorize |
| + code_challenge |
| + code_challenge_method=S256 |
| ----------------------------------> |
| |
| (2) authorization_code |
| <---------------------------------- |
| |
| (3) /token |
| + code |
| + code_verifier |
| ----------------------------------> |
| |
| Server computes: |
| SHA256(code_verifier) |
| == stored code_challenge? |
| |
| (4) access_token |
| <---------------------------------- |
PKCE prevents authorization code interception attacks. Even if an attacker captures the code in transit, they cannot exchange it without the original code_verifier, which never leaves the client.
Client Credentials Flow
The Client Credentials flow is used for machine-to-machine (M2M) communication where no user is involved. Think of a backend microservice calling another internal API, or a cron job syncing data with a third-party service.
+----------+ +---------------+
| | (1) POST /token | |
| Client | grant_type= | Authorization |
| (Server) | client_credentials | Server |
| | client_id=xxx | |
| | client_secret=yyy | |
| | scope=read:data | |
| | -----------------------------> | |
| | | |
| | <----------------------------- | |
| | (2) access_token | |
+----------+ +---------------+
This flow is straightforward: the client authenticates directly with its credentials and receives an access token. There is no user consent step, no redirect, and no authorization code. The token represents the client itself, not a user.
When to use it: Internal service-to-service APIs, automated scripts, CI/CD pipelines accessing protected resources, and IoT backends.
Implicit Flow (Deprecated)
The Implicit flow was designed for browser-based JavaScript apps before PKCE existed. It returns the access token directly in the URL fragment (#access_token=...) after the user consents—skipping the code exchange step entirely.
Why It's Deprecated
- Token exposure: The access token appears in the URL, making it visible in browser history, referrer headers, and proxy logs.
- No refresh tokens: The spec forbids issuing refresh tokens in the Implicit flow, forcing frequent re-authorization.
- Token injection attacks: Without a code exchange, it's easier for attackers to inject stolen tokens into the flow.
- PKCE is the better alternative: With PKCE, public clients can use the Authorization Code flow safely, making Implicit unnecessary.
If you encounter Implicit flow in an existing codebase, plan a migration to Authorization Code + PKCE. Most OAuth providers have already deprecated Implicit in their documentation.
Device Authorization Flow
The Device Authorization flow (RFC 8628) is designed for devices with limited input capabilities—smart TVs, game consoles, CLI tools, and IoT devices. The user authorizes the device using a separate device like their phone or laptop.
+----------+ +---------------+
| | (1) POST /device/code | |
| Device | -----------------------------> | Authorization |
| | | Server |
| | <----------------------------- | |
| | (2) device_code, | |
| | user_code, | |
| | verification_uri | |
+----------+ +---------------+
|
| (3) Display: "Go to https://example.com/device
| and enter code: WDJB-MJHT"
v
+----------+ +---------------+
| User | (4) Opens URL, enters code | |
| (Phone) | -----------------------------> | Authorization |
| | (5) Logs in + consents | Server |
+----------+ +---------------+
|
+----------+ |
| | (6) Poll POST /token |
| Device | grant_type= |
| | device_code |
| | ---------------------------------------->
| | |
| | <----------------------------------------
| | (7) access_token (when approved) |
+----------+ +---------------+
The device polls the token endpoint at a specified interval. The server responds with authorization_pending until the user completes authorization, at which point it returns the access token. This flow is increasingly common in developer CLI tools and streaming devices.
Refresh Tokens: Maintaining Long-Lived Sessions
Access tokens are intentionally short-lived (minutes to hours). Refresh tokens allow clients to obtain new access tokens without requiring the user to re-authenticate. They are long-lived but can be revoked at any time by the Authorization Server.
Refresh Token Rotation
Modern best practice is refresh token rotation: every time a refresh token is used, the Authorization Server issues a new refresh token and invalidates the old one. This provides automatic detection of token theft—if a stolen refresh token is used, the legitimate client's next refresh will fail, signaling a security breach.
The access tokens returned are typically JWT tokens. You can inspect their contents—claims, expiration, and scopes—using a JWT Decoder to verify everything looks correct during development.
Token Storage Best Practices
Where you store tokens is just as important as how you obtain them. The wrong storage mechanism can expose tokens to XSS, CSRF, or other attacks.
Server-Side Applications
- Encrypted server-side sessions: Store tokens in a server-managed session (Redis, database, encrypted cookie). The browser only holds a session ID.
- Never expose tokens to the frontend if the backend can make API calls on behalf of the user.
Single-Page Applications (SPAs)
- Use a Backend-for-Frontend (BFF) pattern: A lightweight backend handles token storage and proxies API calls. The SPA never sees the tokens.
-
If you must store tokens client-side: Use in-memory variables (not
localStorageorsessionStorage). Tokens in storage are accessible to any JavaScript running on the page, including XSS payloads. - HttpOnly, Secure, SameSite cookies: If using cookies, set all three flags. This protects against XSS (HttpOnly) and CSRF (SameSite).
Mobile Applications
- iOS Keychain / Android Keystore: Use platform-specific secure storage designed for sensitive data.
- Never store tokens in shared preferences or plain files.
Storage Comparison
+---------------------+----------+----------+----------+
| Storage Method | XSS Safe | CSRF Safe| Recommended |
+---------------------+----------+----------+----------+
| HttpOnly Cookie | Yes | Partial | Yes* |
| localStorage | No | Yes | No |
| sessionStorage | No | Yes | No |
| In-Memory Variable | Yes | Yes | Yes |
| Server Session | Yes | Yes | Yes |
+---------------------+----------+----------+----------+
* With SameSite=Strict or SameSite=Lax + CSRF token
Common Security Pitfalls
Even with OAuth 2.0's well-defined flows, implementation mistakes are common. Here are the most dangerous ones and how to avoid them:
1. Missing State Parameter
The state parameter prevents CSRF attacks during the authorization flow. Without it, an attacker can trick the user into authorizing the attacker's account, linking it to the victim's session. Always generate a cryptographically random state, store it in the session, and validate it on callback.
2. Open Redirect via redirect_uri
If the Authorization Server does not strictly validate the redirect_uri, an attacker can change it to their own server and capture the authorization code. Register exact redirect URIs (no wildcards) and validate them on both the client and server sides.
3. Insufficient Scope Validation
Requesting overly broad scopes violates the principle of least privilege. If a token with admin:write scope is stolen, the damage is catastrophic. Request only the scopes you need. Validate scopes on the Resource Server for every request.
4. Storing Tokens in localStorage
Any XSS vulnerability on your site can exfiltrate tokens stored in localStorage. This is the single most common token theft vector in SPAs. Use the BFF pattern or HttpOnly cookies instead.
5. Not Validating JWT Signatures
If your Resource Server accepts JWTs without verifying the signature, an attacker can forge tokens with arbitrary claims. Always validate the signature using the Authorization Server's public key (JWKS endpoint). Never use alg: none.
6. Long-Lived Access Tokens Without Revocation
Access tokens that last for days or weeks and cannot be revoked create a large attack window. Keep access token lifetimes short (5–15 minutes) and use refresh token rotation for longer sessions.
7. Mixing Up Authorization and Authentication
OAuth 2.0 access tokens prove authorization, not identity. Using an access token to identify a user (e.g., calling /userinfo and trusting the result for login) without proper OIDC ID token validation can lead to impersonation attacks. Use OpenID Connect with proper ID token validation for authentication.
Implementation Example: Node.js OAuth 2.0 Client
Here's a complete implementation of the Authorization Code flow with PKCE using Express.js. This example demonstrates best practices including state validation, PKCE, and secure token storage.
Setting Up the OAuth Client
Client Credentials Flow (M2M Example)
OAuth 2.0 Providers Comparison
Choosing the right OAuth provider depends on your application's requirements, scale, and budget. Here's a comparison of the most popular providers:
+----------------+---------------+----------------+----------------+
| Provider | Free Tier | Best For | PKCE Support |
+----------------+---------------+----------------+----------------+
| Auth0 | 7,500 MAU | Startups, SPAs | Yes |
| Okta | 100 MAU | Enterprise SSO | Yes |
| Firebase Auth | Unlimited* | Mobile apps | Yes |
| AWS Cognito | 50,000 MAU | AWS ecosystem | Yes |
| Keycloak | Self-hosted | Full control | Yes |
| Google OAuth | Unlimited | Google APIs | Yes |
| GitHub OAuth | Unlimited | Developer tools| Yes |
| Azure AD B2C | 50,000 MAU | Microsoft | Yes |
+----------------+---------------+----------------+----------------+
* Firebase has usage-based pricing for SMS verification
Key Considerations
- Auth0 offers the best developer experience with extensive documentation, quickstarts for every framework, and a generous free tier. Ideal for startups and MVPs.
- AWS Cognito is the best choice if you're already in the AWS ecosystem. The free tier is generous at 50,000 MAU, but the developer experience can be rough.
- Keycloak is the go-to for organizations that need full control over their identity infrastructure. It's open source, feature-rich, and runs on your own servers.
- Firebase Auth shines for mobile-first applications with its native SDKs and tight integration with the Firebase ecosystem.
- Okta/Azure AD are enterprise-focused with advanced features like multi-factor authentication, conditional access, and deep Active Directory integration.
When to Use Which Flow
Selecting the correct OAuth 2.0 flow depends on your client type and deployment context. Here's a decision guide:
Is a user involved?
├── No ──> Client Credentials Flow
│ (M2M, microservices, cron jobs)
│
└── Yes
│
├── Can the client store a secret securely?
│ ├── Yes ──> Authorization Code Flow + PKCE
│ │ (Web backends, confidential clients)
│ │
│ └── No ──> Authorization Code Flow + PKCE
│ (SPAs, mobile apps, public clients)
│
└── Is the device input-constrained?
└── Yes ──> Device Authorization Flow
(Smart TVs, CLIs, IoT)
Notice that Authorization Code + PKCE appears for both confidential and public clients. This is intentional. PKCE adds security with minimal complexity, and the OAuth 2.0 Security BCP recommends it universally. There is no modern scenario where the Implicit flow is recommended.
Quick Reference Table
+----------------------------+------------------+------------------+
| Scenario | Recommended Flow | Refresh Tokens? |
+----------------------------+------------------+------------------+
| Server-rendered web app | Auth Code + PKCE | Yes |
| Single-page application | Auth Code + PKCE | Yes (rotate) |
| Native mobile app | Auth Code + PKCE | Yes (rotate) |
| Backend microservice | Client Creds | No (re-request) |
| CLI / developer tool | Device Auth | Yes |
| Smart TV / game console | Device Auth | Yes |
| Legacy browser app | Auth Code + PKCE | Migrate away |
+----------------------------+------------------+------------------+
Best Practices Checklist
Use this checklist before deploying any OAuth 2.0 implementation to production:
Authorization Flow
- Use Authorization Code + PKCE for all user-facing flows
- Generate a cryptographically random
stateparameter and validate it on callback - Register exact
redirect_urivalues—no wildcards, no open redirects - Request the minimum required scopes (principle of least privilege)
- Validate all redirect URIs server-side before issuing codes
Token Management
- Keep access token lifetimes short (5–15 minutes)
- Implement refresh token rotation
- Store tokens server-side whenever possible (BFF pattern for SPAs)
- Never store tokens in
localStorageorsessionStorage - Set
HttpOnly,Secure, andSameSiteflags on all cookies - Implement token revocation on logout
Security
- Validate JWT signatures using the provider's JWKS endpoint
- Verify the
aud(audience),iss(issuer), andexp(expiration) claims on every token - Use TLS (HTTPS) for all OAuth 2.0 endpoints—no exceptions
- Implement rate limiting on token endpoints to prevent brute-force attacks
- Log authentication events for security monitoring and incident response
- Rotate client secrets periodically
- Never log tokens—use token identifiers or hashes for debugging
Error Handling
- Handle all OAuth error responses gracefully (
invalid_grant,invalid_client, etc.) - Implement automatic retry with exponential backoff for transient token endpoint failures
- Force re-authentication when refresh token rotation detects reuse
- Return generic error messages to end users—never expose OAuth error details in the UI
Conclusion
OAuth 2.0 remains the foundation of modern API authorization. While the protocol offers multiple flows for different scenarios, the industry is converging on a clear recommendation: use Authorization Code + PKCE for all interactive flows and Client Credentials for machine-to-machine communication. The Implicit flow is officially deprecated, and the Device Authorization flow fills the gap for input-constrained devices.
The most important takeaway is that OAuth 2.0 security depends as much on implementation quality as on flow selection. Proper state validation, secure token storage, short token lifetimes, refresh token rotation, and JWT signature verification are all non-negotiable in production systems. Use the checklist above as your pre-launch security review, and always stay current with the OAuth 2.0 Security Best Current Practice RFC.
If you're working with JWT-based access tokens, use our JWT Decoder to inspect token claims during development. For a deeper dive into JWT structure and validation, read our guide on JWT Token Explained.
Free Developer Tools
If you found this article helpful, check out DevToolkit — 40+ free browser-based developer tools with no signup required.
Popular tools: JSON Formatter · Regex Tester · JWT Decoder · Base64 Encoder
🛒 Get the DevToolkit Starter Kit on Gumroad — source code, deployment guide, and customization templates.
Top comments (0)