OAuth & JWT Authentication
oauth jwt authentication api security
OAuth 2.0 Overview
OAuth 2.0 is an authorization framework (not authentication). It allows apps to obtain limited access to user accounts.
Key Roles
| Role | Description |
|---|---|
| Resource Owner | The user who owns the data |
| Client | The application requesting access |
| Authorization Server | Issues tokens (e.g., Okta, Auth0, Azure AD) |
| Resource Server | API that holds protected resources |
OAuth 2.0 Grant Types
1. Authorization Code (Most Secure for Web Apps)
Best for: Server-side web applications
┌──────────┐ ┌───────────────────┐
│ User │ │ Auth Server │
└────┬─────┘ └─────────┬─────────┘
│ │
│ 1. Click "Login" │
│─────────────────────────────────────────────>│
│ │
│ 2. Redirect to Auth Server │
│<─────────────────────────────────────────────│
│ │
│ 3. User logs in, consents │
│─────────────────────────────────────────────>│
│ │
│ 4. Redirect back with auth code │
│<─────────────────────────────────────────────│
│ │
│ ┌─────────────┐ │
│ │ Client │ │
│ │ Server │ │
│ └──────┬──────┘ │
│ │ │
│ │ 5. Exchange code for token │
│ │ (with client_secret) │
│ │────────────────────────────>│
│ │ │
│ │ 6. Access token + refresh │
│ │<────────────────────────────│
Token Request:
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https://app.example.com/callback
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET2. Authorization Code with PKCE (Mobile/SPA)
Best for: Mobile apps, single-page apps (no client secret)
Additional parameters:
code_verifier: Random string (43-128 chars)code_challenge: Base64URL(SHA256(code_verifier))
# Authorization request includes:
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
# Token request includes:
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk3. Client Credentials (Machine-to-Machine)
Best for: Server-to-server, no user involved
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRET
&scope=read:data write:dataResponse:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}4. Refresh Token
Exchange a refresh token for a new access token:
POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=REFRESH_TOKEN
&client_id=CLIENT_ID
&client_secret=CLIENT_SECRETJWT (JSON Web Token)
Structure
header.payload.signature
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE1MTYyMzkwMjJ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header (Base64URL decoded)
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-123"
}| Field | Description |
|---|---|
alg | Algorithm: RS256, HS256, ES256 |
typ | Token type (usually “JWT”) |
kid | Key ID (for key rotation) |
Payload (Claims)
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "api.example.com",
"exp": 1735689600,
"iat": 1735686000,
"nbf": 1735686000,
"scope": "read write",
"roles": ["admin", "user"]
}Registered Claims:
| Claim | Description |
|---|---|
iss | Issuer |
sub | Subject (user ID) |
aud | Audience (intended recipient) |
exp | Expiration time (Unix timestamp) |
iat | Issued at |
nbf | Not before |
jti | JWT ID (unique identifier) |
Using Tokens
Bearer Token in Request
GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...Token Validation Checklist
- Verify signature - Use issuer’s public key
- Check
exp- Token not expired - Check
nbf- Token is active - Check
iss- Expected issuer - Check
aud- Your API is intended audience - Check
scope/claims - Has required permissions
Token Lifetimes (Best Practices)
| Token Type | Typical Lifetime | Storage |
|---|---|---|
| Access Token | 15 min - 1 hour | Memory only |
| Refresh Token | Days - weeks | Secure storage (httpOnly cookie, keychain) |
| ID Token | 1 hour | Memory only |
Common Errors
401 Unauthorized
| Error | Cause | Fix |
|---|---|---|
invalid_token | Token expired or malformed | Refresh or re-authenticate |
insufficient_scope | Missing required scope | Request additional scopes |
Token Request Errors
| Error | Cause |
|---|---|
invalid_client | Wrong client_id/secret |
invalid_grant | Auth code expired or already used |
invalid_scope | Requested scope not allowed |
unauthorized_client | Client not allowed this grant type |
Decode JWT (Without Verifying)
Command line:
# Decode header
echo "eyJhbGciOiJSUzI1NiIs..." | cut -d. -f1 | base64 -d 2>/dev/null | jq
# Decode payload
echo "eyJhbGciOiJSUzI1NiIs..." | cut -d. -f2 | base64 -d 2>/dev/null | jqOnline: https://jwt.io (don’t paste production tokens!)
Python:
import jwt
decoded = jwt.decode(token, options={"verify_signature": False})OIDC (OpenID Connect)
OIDC adds authentication layer on top of OAuth 2.0.
Additional features:
id_token- Contains user identity claims/userinfoendpoint - Get user profile- Standard claims:
name,email,picture, etc.
ID Token payload:
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "client-app-id",
"exp": 1735689600,
"iat": 1735686000,
"nonce": "abc123",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true
}Security Best Practices
| Do | Don’t |
|---|---|
| Use HTTPS everywhere | Store tokens in localStorage (XSS risk) |
| Validate all claims | Trust tokens without verification |
| Use short-lived access tokens | Use long-lived access tokens |
| Rotate refresh tokens | Reuse refresh tokens indefinitely |
| Use PKCE for public clients | Use implicit flow |
| Validate redirect URIs strictly | Allow open redirects |