Last updated
Authorization Code Flow — Step by Step
The most common OAuth 2.0 flow for server-side applications:
Step 1: Build the authorization URL
GET https://auth.example.com/oauth/authorize?
response_type=code
&client_id=my_client_id
&redirect_uri=https://myapp.com/callback
&scope=read:user read:email
&state=xK9mP2qR7vN4wL8j (random CSRF token)
Step 2: User logs in and approves — server redirects to:
https://myapp.com/callback?
code=SplxlOBeZQQYbYS6WxSbIA
&state=xK9mP2qR7vN4wL8j
Step 3: Exchange code for tokens
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://myapp.com/callback
&client_id=my_client_id
&client_secret=my_client_secret
Step 4: Token response
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read:user read:email"
}
PKCE — Authorization Code with Proof Key
The secure extension for public clients (SPAs, mobile apps):
Step 1: Generate code verifier and challenge
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = BASE64URL(SHA256(code_verifier))
= "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Step 2: Authorization request (include challenge)
GET https://auth.example.com/oauth/authorize?
response_type=code
&client_id=my_spa_client
&redirect_uri=https://myapp.com/callback
&scope=read:user
&state=xK9mP2qR7vN4wL8j
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Step 3: Token exchange (include verifier, no client_secret needed)
POST https://auth.example.com/oauth/token
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://myapp.com/callback
&client_id=my_spa_client
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
PKCE prevents authorization code interception attacks. Even if an attacker intercepts the code, they cannot exchange it without the code verifier.
Client Credentials Flow (Machine-to-Machine)
For server-to-server API calls with no user involved:
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)
grant_type=client_credentials
&scope=api:read api:write
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "api:read api:write"
}
No refresh token is issued — just request a new access token when the current one expires. This flow is used for microservice-to-microservice authentication and background jobs.
Token Refresh Flow
Get a new access token using a refresh token (no user re-authentication needed):
POST https://auth.example.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA
&client_id=my_client_id
&client_secret=my_client_secret
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...NEW...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "new_refresh_token_value" // Rotation
}
Refresh token rotation issues a new refresh token with each use. Store the new refresh token and discard the old one. If the old token is used again, it indicates token theft and the server should revoke all tokens.
JWT Access Token Inspection
Decode a JWT to see its claims:
Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfMTIzIiwic2NvcGUiOiJyZWFkOnVzZXIiLCJleHAiOjE3MTA2OTYwMDB9.signature
Header:
{
"alg": "RS256",
"typ": "JWT"
}
Payload:
{
"sub": "usr_123", // Subject (user ID)
"iss": "https://auth.example.com",
"aud": "https://api.example.com",
"scope": "read:user read:email",
"exp": 1710696000, // Expires: 2024-03-17T12:00:00Z
"iat": 1710692400, // Issued at
"jti": "V1StGXR8_Z5jdHi6B" // JWT ID (for revocation)
}
The playground decodes the token and shows the expiration time in human-readable format. Check that the scope contains the permissions your API requires before accepting the token.
State Parameter — CSRF Protection
The state parameter prevents cross-site request forgery attacks:
// 1. Generate random state before redirecting
const state = crypto.randomBytes(16).toString('hex');
// state = "a3f7b2c1d4e5f6a7b8c9d0e1f2a3b4c5"
// 2. Store in session
session.oauthState = state;
// 3. Include in authorization URL
const authUrl = `https://auth.example.com/authorize?...&state=${state}`;
// 4. In callback — verify state matches
if (req.query.state !== session.oauthState) {
return res.status(403).send('CSRF attack detected');
}
// Proceed with code exchange
OAuth Error Responses
Common error responses and what they mean:
// User denied authorization
https://myapp.com/callback?
error=access_denied
&error_description=The+user+denied+access
&state=xK9mP2qR7vN4wL8j
// Invalid client credentials
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}
// Authorization code expired (codes typically expire in 10 minutes)
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}
// Requested scope not available
{
"error": "invalid_scope",
"error_description": "The requested scope is invalid or unknown"
}
Device Authorization Flow
For devices without a browser (smart TVs, CLI tools):
Step 1: Request device code
POST https://auth.example.com/oauth/device/code
client_id=my_device_client
&scope=read:user
Response:
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://auth.example.com/device",
"expires_in": 900,
"interval": 5
}
Step 2: Show user_code to user
"Visit https://auth.example.com/device and enter: WDJB-MJHT"
Step 3: Poll for token every 5 seconds
POST https://auth.example.com/oauth/token
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=my_device_client
// While waiting: { "error": "authorization_pending" }
// After approval: { "access_token": "...", ... }
Using the Access Token
Include the token in API requests as a Bearer token:
GET https://api.example.com/user/profile
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
// Response
{
"id": "usr_123",
"name": "[name]",
"email": "[email]"
}
Never put access tokens in URL query parameters — they appear in server logs and browser history. Always use the Authorization header.