Table of Contents
- Understanding OAuth 2.0: Core Concepts
- 1.1 What is OAuth 2.0?
- 1.2 OAuth 2.0 vs. Authentication (OpenID Connect)
- Core Components of OAuth 2.0
- 2.1 Resource Owner
- 2.2 Client
- 2.3 Authorization Server
- 2.4 Resource Server
- 2.5 Tokens: Access Tokens and Refresh Tokens
- OAuth 2.0 Flows: Which One to Use?
- 3.1 Authorization Code Flow (Most Secure)
- 3.2 Authorization Code Flow with PKCE (For Mobile/Native Apps)
- 3.3 Client Credentials Flow (Server-to-Server)
- 3.4 Implicit Flow (Legacy, Avoid for New Apps)
- 3.5 Resource Owner Password Credentials Flow (Trusted Apps Only)
- Step-by-Step Implementation Guide
- 4.1 Prerequisites
- 4.2 Step 1: Choose/Set Up an Authorization Server
- 4.3 Step 2: Register Your Client Application
- 4.4 Step 3: Implement the Authorization Request
- 4.5 Step 4: Handle User Authentication and Consent
- 4.6 Step 5: Exchange Authorization Code for Tokens
- 4.7 Step 6: Validate Access Tokens on the Resource Server
- 4.8 Step 7: Handle Token Expiry and Refresh
- Security Best Practices
- Troubleshooting Common Issues
- Conclusion
- References
1. Understanding OAuth 2.0: Core Concepts
1.1 What is OAuth 2.0?
OAuth 2.0 is an authorization framework that allows a third-party application (the “client”) to access protected resources (e.g., user data) on behalf of a user (the “resource owner”) without sharing the user’s credentials. Instead, it uses tokens to grant temporary, scoped access.
Example: When you click “Sign in with Google” on a app, OAuth 2.0 lets the app request access to your Google profile (e.g., name, email) without you sharing your Google password with the app.
1.2 OAuth 2.0 vs. Authentication (OpenID Connect)
OAuth 2.0 is authorization-only (it answers “what can the client do?”). For authentication (“who is the user?”), OAuth 2.0 is often extended with OpenID Connect (OIDC), an identity layer built on OAuth 2.0. OIDC adds an id_token (a JWT) to confirm the user’s identity.
This guide focuses on OAuth 2.0 authorization, but many concepts overlap with OIDC.
2. Core Components of OAuth 2.0
To implement OAuth 2.0, you need to understand these key roles:
2.1 Resource Owner
The user (or system) that owns the protected resources (e.g., a user with a Google account). They grant permission to the client to access their data.
2.2 Client
The application requesting access to the resource owner’s data (e.g., a mobile app, web app, or backend service). The client must be registered with the authorization server.
2.3 Authorization Server
A server that authenticates the resource owner, obtains their consent, and issues access tokens (and optionally refresh tokens) to the client. Examples: Google Auth, Okta, Auth0, or self-hosted tools like Keycloak.
2.4 Resource Server
The server hosting the protected resources (e.g., a REST API for user data, a photo storage service). It accepts access tokens and returns resources if the token is valid.
2.5 Tokens: Access Tokens and Refresh Tokens
- Access Token: A short-lived token (e.g., 15–60 minutes) that the client uses to request resources from the resource server. Format: Often a JWT (JSON Web Token) or opaque string.
- Refresh Token: A longer-lived token (e.g., days/weeks) used to request new access tokens when the old one expires. Stored securely by the client (never exposed to the frontend).
3. OAuth 2.0 Flows: Which One to Use?
OAuth 2.0 defines several “flows” (authorization sequences) tailored to different client types. Choose the flow based on whether your client is public (e.g., mobile app, SPA) or confidential (e.g., backend service with a server).
3.1 Authorization Code Flow (Most Secure)
Use case: Confidential clients (web apps with a backend server).
Why: It involves an intermediate “authorization code” to exchange for tokens, reducing the risk of token interception.
High-level steps:
- Client redirects the user to the authorization server.
- User authenticates and consents; authorization server returns an
authorization_code. - Client exchanges the
authorization_code(plus a client secret) for anaccess_tokenandrefresh_token.
3.2 Authorization Code Flow with PKCE (For Mobile/Native Apps)
Use case: Public clients (mobile apps, SPAs, desktop apps) that can’t securely store a client secret.
What is PKCE?: Proof Key for Code Exchange (PKCE, RFC 7636) adds a cryptographic challenge to prevent code interception attacks. It replaces the client secret with a code_verifier and code_challenge.
3.3 Client Credentials Flow
Use case: Server-to-server communication (no user involvement).
Example: A backend service accessing an API for analytics data (not user-specific).
How it works: Client sends its client_id and client_secret directly to the authorization server to get an access_token.
3.4 Implicit Flow (Legacy, Avoid for New Apps)
Use case: Legacy SPAs (no backend).
Why avoid: Returns the access_token directly in the URL fragment, increasing interception risk. Replaced by Authorization Code Flow with PKCE.
3.5 Resource Owner Password Credentials Flow (Trusted Apps Only)
Use case: Highly trusted clients (e.g., your own app’s official mobile client).
Risk: Requires the user to share their password with the client. Use only if no other flow is feasible.
Recommendation: For most modern apps, use Authorization Code Flow with PKCE (public clients) or Authorization Code Flow (confidential clients).
4. Step-by-Step Implementation Guide
We’ll implement the Authorization Code Flow with PKCE (most versatile) for a hypothetical scenario:
- Client: A mobile app (public client) wanting to access a user’s photos from a “PhotoAPI” (resource server).
- Authorization Server: Auth0 (a popular third-party auth provider).
4.1 Prerequisites
- A client application (e.g., mobile app, web app).
- Access to an authorization server (e.g., Auth0, Okta, Keycloak, or self-hosted).
- A resource server (e.g., PhotoAPI) that accepts OAuth 2.0 access tokens.
4.2 Step 1: Choose/Set Up an Authorization Server
For simplicity, use a managed service like Auth0 or Okta instead of building your own (self-hosting is complex and error-prone).
Example with Auth0:
- Sign up for Auth0 and create a tenant.
- Navigate to Applications > Create Application and select “Native” (for mobile) or “Regular Web App” (for confidential clients).
4.3 Step 2: Register Your Client Application
Clients must be registered with the authorization server to receive a client_id (public identifier) and, for confidential clients, a client_secret (keep this secret!).
Registration details required:
client_id: Unique ID for your client (public).redirect_uris: URLs the authorization server will redirect to after user consent (e.g.,com.yourapp://callbackfor mobile).scopes: Permissions the client requests (e.g.,read:photosfor PhotoAPI).grant_types: Flows the client will use (e.g.,authorization_code).
Example (Auth0):
After creating the app, note the client_id and add your redirect_uri (e.g., com.yourapp://callback).
4.4 Step 3: Implement the Authorization Request
The client initiates the flow by redirecting the user to the authorization server’s /authorize endpoint with the following parameters (for PKCE):
| Parameter | Description |
|---|---|
response_type | Must be code (for Authorization Code Flow). |
client_id | Your registered client_id. |
redirect_uri | Must match one of the registered redirect URIs. |
scope | Space-separated permissions (e.g., openid profile read:photos). |
state | Random string to prevent CSRF (validate in the callback). |
code_challenge | PKCE: Base64URL-encoded SHA-256 hash of the code_verifier. |
code_challenge_method | PKCE: S256 (SHA-256) or plain (avoid plain). |
Example Authorization URL (Auth0):
https://your-tenant.auth0.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=com.yourapp://callback
&scope=read:photos
&state=random-123456
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Generating PKCE Code Verifier/Challenge:
- Generate a random
code_verifier(e.g., 64-character alphanumeric string). - Compute
code_challengeasBASE64URL(SHA256(code_verifier)).
Example (Python):
import base64
import hashlib
import os
code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode('utf-8').rstrip('=')
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode('utf-8')).digest()).decode('utf-8').rstrip('=')
4.5 Step 4: Handle User Authentication and Consent
The authorization server redirects the user to its login page. After the user authenticates (e.g., enters their password), the server prompts them to consent to the requested scopes (e.g., “Allow App to read your photos?”).
If the user consents, the authorization server redirects back to the client’s redirect_uri with an authorization_code and the state parameter (to validate against CSRF).
4.6 Step 5: Exchange Authorization Code for Tokens
The client now sends the authorization_code to the authorization server’s /oauth/token endpoint to get access_token, refresh_token, and expires_in (token lifetime).
Request Parameters (PKCE):
| Parameter | Description |
|---|---|
grant_type | authorization_code. |
client_id | Your client_id. |
redirect_uri | Must match the one used in Step 3. |
code | The authorization_code received from the redirect. |
code_verifier | PKCE: The original code_verifier (not the challenge). |
Example cURL Request:
curl -X POST https://your-tenant.auth0.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "redirect_uri=com.yourapp://callback" \
-d "code=AUTHORIZATION_CODE_FROM_REDIRECT" \
-d "code_verifier=YOUR_CODE_VERIFIER"
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"scope": "read:photos"
}
4.7 Step 6: Validate Access Tokens on the Resource Server
The resource server (e.g., PhotoAPI) must validate the access_token before returning resources.
How to validate:
- JWT Tokens: If the
access_tokenis a JWT, verify its signature using the authorization server’s public key (found at/.well-known/jwks.json). Check claims likeexp(expiry),iss(issuer), andscope. - Opaque Tokens: If the token is opaque (random string), the resource server calls the authorization server’s
/introspectendpoint with the token to check validity.
Example JWT Validation (Node.js/Express):
const jwt = require('express-jwt');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://your-tenant.auth0.com/.well-known/jwks.json'
});
const validateJWT = jwt({
secret: jwksClient.expressJwtSecret({ cache: true, jwksUri: client.options.jwksUri }),
audience: 'https://photoapi.com', // Your resource server's identifier
issuer: 'https://your-tenant.auth0.com/',
algorithms: ['RS256']
});
// Protect your API endpoint
app.get('/photos', validateJWT, (req, res) => {
res.json({ photos: [...] }); // Return resources if token is valid
});
4.8 Step 7: Handle Token Expiry and Refresh
Access tokens expire (e.g., after 1 hour). Use the refresh_token to get a new access_token without re-authenticating the user.
Refresh Token Request:
curl -X POST https://your-tenant.auth0.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=YOUR_CLIENT_ID" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "code_verifier=YOUR_CODE_VERIFIER" # Only for PKCE clients
Response:
{
"access_token": "new-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600
}
5. Security Best Practices
- Use HTTPS Everywhere: All OAuth 2.0 communication (authorization requests, token exchanges) must use HTTPS to prevent interception.
- Store Secrets Securely:
- Confidential clients: Store
client_secretin environment variables (never in code or frontend). - Public clients: Use PKCE instead of client secrets.
- Refresh tokens: Store in secure, encrypted storage (e.g., Keychain for iOS, Keystore for Android).
- Confidential clients: Store
- Validate Redirect URIs: Authorization servers should enforce strict redirect URI validation to prevent open redirects.
- Limit Scopes: Request only the scopes your app needs (e.g.,
read:photosinstead of*). - Short-Lived Access Tokens: Reduce exposure if tokens are leaked (e.g., 15–60 minutes).
- Use State Parameter: Always include
statein authorization requests to prevent CSRF. - Avoid Implicit Flow: Use Authorization Code Flow with PKCE for SPAs/mobile apps.
6. Troubleshooting Common Issues
- “Invalid redirect URI”: Ensure the
redirect_uriin the authorization request matches one registered with the authorization server. - “Access token rejected by resource server”:
- Check if the token is expired.
- Verify the token’s signature/introspection response.
- Ensure the token includes the required
scope.
- “Refresh token invalid”: Refresh tokens may be revoked (e.g., user logs out). Re-initiate the authorization flow.
- CSRF Errors: Missing
stateparameter or mismatch in the callback.
7. Conclusion
OAuth 2.0 is a powerful framework for secure authorization, but its complexity requires careful implementation. By choosing the right flow (e.g., Authorization Code with PKCE), validating tokens rigorously, and following security best practices, you can protect user data while enabling seamless third-party integrations.
For most developers, using managed authorization servers (Auth0, Okta) is preferable to building your own. Focus on integrating OAuth 2.0 correctly rather than reinventing the wheel!