# OAuth2 and OpenID Connect
Comprehensive reference for OAuth2 grant types, OIDC, provider integration, and social login.
## OAuth2 Core Concepts
### Roles
| Role | Description | Example |
|------|-------------|---------|
| **Resource Owner** | The user who owns the data | End user |
| **Client** | The application requesting access | Your web/mobile app |
| **Authorization Server** | Issues tokens after authentication | Auth0, Keycloak, your auth service |
| **Resource Server** | Hosts the protected API | Your API server |
### Key Terms
| Term | Description |
|------|-------------|
| **Scope** | Permission level requested (e.g., `read:users`, `write:posts`) |
| **Grant Type** | The flow used to obtain tokens |
| **Authorization Code** | Temporary code exchanged for tokens |
| **Access Token** | Token used to call the API |
| **Refresh Token** | Token used to get new access tokens |
| **Redirect URI** | Where the authorization server sends the user back |
| **State** | CSRF protection parameter (random, unguessable) |
| **PKCE** | Proof Key for Code Exchange (prevents code interception) |
## Authorization Code + PKCE
The recommended flow for web applications, mobile apps, and SPAs. PKCE (Proof Key for Code Exchange) protects against authorization code interception.
### Flow
```
┌──────┐ ┌───────────────┐ ┌──────────────┐
│Client│ │ Authorization │ │ Resource │
│ │ │ Server │ │ Server │
└──┬───┘ └───────┬───────┘ └──────┬───────┘
│ │ │
│ 1. Generate code_verifier (random) │
│ code_challenge = SHA256(code_verifier) │
│ │ │
│ 2. Redirect to /authorize │
│ ?response_type=code │
│ &client_id=xxx │
│ &redirect_uri=https://app/callback │
│ &scope=openid profile email │
│ &state=random_csrf_value │
│ &code_challenge=xxx │
│ &code_challenge_method=S256 │
│──────────────>│ │
│ │ │
│ 3. User authenticates and consents │
│ │ │
│ 4. Redirect to callback │
│ ?code=authorization_code │
│ &state=random_csrf_value │
│<──────────────│ │
│ │
│ 5. POST /token │
│ grant_type=authorization_code │
│ &code=authorization_code │
│ &redirect_uri=https://app/callback │
│ &client_id=xxx │
│ &code_verifier=original_random_value │
│──────────────>│ │
│ │ │
│ 6. Response: access_token, refresh_token, │
│ id_token (if OIDC) │
│<──────────────│ │
│ │
│ 7. GET /api/resource │
│ Authorization: Bearer access_token │
│────────────────────────────────────────────────>│
│ │
│ 8. Response: protected resource │
│<────────────────────────────────────────────────│
```
### Implementation: Node.js
```javascript
import crypto from 'crypto';
// Step 1: Generate PKCE values
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Step 2: Build authorization URL
function getAuthorizationUrl(config) {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
// Store verifier and state in session
// req.session.pkceVerifier = verifier;
// req.session.oauthState = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: 'openid profile email',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
return `${config.authorizationEndpoint}?${params}`;
}
// Step 5: Exchange code for tokens
async function exchangeCode(code, verifier, config) {
const response = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret, // Confidential clients only
code_verifier: verifier,
}),
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status}`);
}
return response.json();
// Returns: { access_token, refresh_token, id_token, token_type, expires_in }
}
```
### Implementation: Python
```python
import hashlib
import secrets
import base64
from urllib.parse import urlencode
import httpx
def generate_pkce():
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
return verifier, challenge
def get_authorization_url(config: dict) -> tuple[str, str, str]:
verifier, challenge = generate_pkce()
state = secrets.token_hex(16)
params = urlencode({
"response_type": "code",
"client_id": config["client_id"],
"redirect_uri": config["redirect_uri"],
"scope": "openid profile email",
"state": state,
"code_challenge": challenge,
"code_challenge_method": "S256",
})
url = f"{config['authorization_endpoint']}?{params}"
return url, verifier, state
async def exchange_code(code: str, verifier: str, config: dict) -> dict:
async with httpx.AsyncClient() as client:
response = await client.post(
config["token_endpoint"],
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": config["redirect_uri"],
"client_id": config["client_id"],
"client_secret": config["client_secret"],
"code_verifier": verifier,
},
)
response.raise_for_status()
return response.json()
```
### Redirect URI Validation
**Critical security requirement:** The authorization server must validate redirect URIs exactly.
| Rule | Why |
|------|-----|
| Exact match required | Prevents open redirect attacks |
| No wildcards in production | Attacker could register matching subdomain |
| HTTPS required | Prevent code interception on HTTP |
| No fragments (#) | Fragment not sent to server |
| Pre-register all URIs | Only allow known, trusted redirect targets |
### State Parameter
The `state` parameter prevents CSRF attacks on the OAuth2 flow:
```javascript
// Before redirect: generate and store
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
// In callback: validate
if (req.query.state !== req.session.oauthState) {
throw new Error('State mismatch - possible CSRF attack');
}
delete req.session.oauthState;
```
## Client Credentials Grant
Server-to-server authentication with no user context.
```
┌──────────┐ ┌───────────────┐
│ Service │ │ Authorization │
│ Client │ │ Server │
└─────┬─────┘ └───────┬───────┘
│ │
│ POST /token │
│ grant_type=client_credentials │
│ &client_id=xxx │
│ &client_secret=yyy │
│ &scope=read:data │
│─────────────────────────────────>│
│ │
│ { access_token, expires_in } │
│<─────────────────────────────────│
```
```javascript
// Node.js implementation
async function getClientCredentialsToken(config) {
const response = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${Buffer.from(
`${config.clientId}:${config.clientSecret}`
).toString('base64')}`,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: config.scope,
}),
});
const data = await response.json();
// Cache the token until near expiry
// tokenCache.set(cacheKey, data.access_token, data.expires_in - 60);
return data.access_token;
}
```
```python
# Python implementation
async def get_client_credentials_token(config: dict) -> str:
async with httpx.AsyncClient() as client:
response = await client.post(
config["token_endpoint"],
auth=(config["client_id"], config["client_secret"]),
data={
"grant_type": "client_credentials",
"scope": config["scope"],
},
)
response.raise_for_status()
data = response.json()
return data["access_token"]
```
**Best practices:**
- Cache tokens until near expiry (subtract 60 seconds from `expires_in`)
- Use mutual TLS (mTLS) for additional security in high-trust environments
- Rotate client secrets periodically
## Device Code Grant
For CLI tools, smart TVs, and devices without a browser or with limited input.
```
┌──────────┐ ┌───────────────┐ ┌──────────┐
│ Device │ │ Authorization │ │ User's │
│ (CLI/TV) │ │ Server │ │ Browser │
└─────┬─────┘ └───────┬───────┘ └────┬─────┘
│ │ │
│ POST /device/code │ │
│ client_id=xxx │ │
│ scope=profile │ │
│───────────────────────>│ │
│ │ │
│ { device_code, │ │
│ user_code: "ABCD-1234", │
│ verification_uri, │ │
│ interval: 5 } │ │
│<───────────────────────│ │
│ │ │
│ Display to user: │ │
│ "Visit https://auth.example.com/device" │
│ "Enter code: ABCD-1234"│ │
│ │ │
│ │ User visits URL │
│ │ and enters code │
│ │<──────────────────────│
│ │ │
│ │ User authenticates │
│ │ and authorizes │
│ │<──────────────────────│
│ │ │
│ Poll: POST /token │ │
│ grant_type=urn:ietf: │ │
│ params:oauth: │ │
│ grant-type:device_code │
│ device_code=xxx │ │
│───────────────────────>│ │
│ │ │
│ { access_token } │ │
│<───────────────────────│ │
```
```javascript
// CLI implementation
async function deviceCodeFlow(config) {
// 1. Request device code
const codeResponse = await fetch(`${config.authServer}/device/code`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: config.clientId,
scope: 'openid profile',
}),
});
const { device_code, user_code, verification_uri, interval } =
await codeResponse.json();
// 2. Display to user
console.log(`Visit: ${verification_uri}`);
console.log(`Enter code: ${user_code}`);
// 3. Poll for completion
while (true) {
await new Promise((r) => setTimeout(r, interval * 1000));
const tokenResponse = await fetch(`${config.authServer}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
device_code,
client_id: config.clientId,
}),
});
const data = await tokenResponse.json();
if (data.error === 'authorization_pending') continue;
if (data.error === 'slow_down') {
interval += 5;
continue;
}
if (data.error) throw new Error(data.error_description);
return data; // { access_token, refresh_token, ... }
}
}
```
## Token Exchange (RFC 8693)
Allows a service to exchange one token for another, maintaining user context across microservices.
```javascript
// Service A has user's token, needs to call Service B
async function exchangeToken(userToken, targetAudience, config) {
const response = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
subject_token: userToken,
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
audience: targetAudience, // Service B's identifier
scope: 'read:data',
}),
});
return response.json();
// Returns token with `act` claim showing delegation chain:
// { "sub": "user_123", "act": { "sub": "service_a" } }
}
```
## OpenID Connect (OIDC)
OIDC is an identity layer on top of OAuth2. While OAuth2 handles authorization (access to resources), OIDC handles authentication (who the user is).
### What OIDC Adds to OAuth2
| OAuth2 Only | OIDC Adds |
|-------------|-----------|
| Access token (opaque) | ID token (JWT with user info) |
| Resource access | User identity |
| Scopes for permissions | Standard identity scopes |
| No user info standard | UserInfo endpoint |
| No discovery | `.well-known/openid-configuration` |
### ID Token
The ID token is a JWT containing user identity information.
```json
{
"iss": "https://auth.example.com",
"sub": "user_abc123",
"aud": "client_id_xyz",
"exp": 1700001500,
"iat": 1700000600,
"nonce": "random_nonce_value",
"auth_time": 1700000500,
"name": "Alice Smith",
"email": "alice@example.com",
"email_verified": true,
"picture": "https://example.com/alice.jpg"
}
```
### Standard OIDC Scopes
| Scope | Claims Returned |
|-------|----------------|
| `openid` | `sub` (required scope for OIDC) |
| `profile` | `name`, `family_name`, `given_name`, `picture`, `locale` |
| `email` | `email`, `email_verified` |
| `address` | `address` (structured object) |
| `phone` | `phone_number`, `phone_number_verified` |
### Discovery Document
```
GET https://auth.example.com/.well-known/openid-configuration
```
```json
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"userinfo_endpoint": "https://auth.example.com/userinfo",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "client_credentials"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"code_challenge_methods_supported": ["S256"]
}
```
### UserInfo Endpoint
```javascript
// Fetch additional user info
const userInfo = await fetch('https://auth.example.com/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());
// Response:
// {
// "sub": "user_abc123",
// "name": "Alice Smith",
// "email": "alice@example.com",
// "email_verified": true,
// "picture": "https://example.com/alice.jpg"
// }
```
## Provider Integration
### Auth0
```javascript
// Next.js with Auth0 SDK
// npm install @auth0/nextjs-auth0
// app/api/auth/[auth0]/route.ts
import { handleAuth } from '@auth0/nextjs-auth0';
export const GET = handleAuth();
// app/layout.tsx
import { UserProvider } from '@auth0/nextjs-auth0/client';
export default function RootLayout({ children }) {
return