Comprehensive technical reference for JWT, OAuth 2.0, and API authentication specifications.
Comprehensive technical reference for JWT, OAuth 2.0, and API authentication specifications.
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure.
Specification: RFC 7519
BASE64URL(HEADER).BASE64URL(PAYLOAD).BASE64URL(SIGNATURE)
Example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Required:
alg: Algorithm used for signing (MUST be validated)typ: Token type (typically "JWT")Optional:
kid: Key ID hint indicating which key was usedcty: Content type (only used for nested JWTs)Algorithms:
HMAC:
HS256 (HMAC with SHA-256) - Symmetric
HS384 (HMAC with SHA-384) - Symmetric
HS512 (HMAC with SHA-512) - Symmetric
RSA:
RS256 (RSA with SHA-256) - Asymmetric
RS384 (RSA with SHA-384) - Asymmetric
RS512 (RSA with SHA-512) - Asymmetric
ECDSA:
ES256 (ECDSA with P-256 and SHA-256) - Asymmetric
ES384 (ECDSA with P-384 and SHA-384) - Asymmetric
ES512 (ECDSA with P-521 and SHA-512) - Asymmetric
None:
none (Unsecured JWT) - DANGEROUS, NEVER USE IN PRODUCTION
Standard claims per RFC 7519:
| Claim | Name | Type | Description | |-------|------|------|-------------| | iss | Issuer | String | Principal that issued the JWT | | sub | Subject | String | Principal that is the subject (user ID) | | aud | Audience | String/Array | Recipients that the JWT is intended for | | exp | Expiration Time | NumericDate | Expiration time after which JWT is invalid | | nbf | Not Before | NumericDate | Time before which JWT is invalid | | iat | Issued At | NumericDate | Time at which JWT was issued | | jti | JWT ID | String | Unique identifier for the JWT |
NumericDate: JSON numeric value representing seconds since Unix epoch (1970-01-01T00:00:00Z UTC)
Example Payload:
{
"iss": "https://auth.example.com",
"sub": "user_12345",
"aud": ["https://api.example.com", "https://app.example.com"],
"exp": 1735689600,
"nbf": 1735686000,
"iat": 1735686000,
"jti": "unique-token-id-123",
"custom_claim": "custom_value"
}
HMAC (HS256):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
RSA (RS256):
RSASSA-PKCS1-v1_5 using SHA-256
ECDSA (ES256):
ECDSA using P-256 curve and SHA-256
MUST:
exp claim (reject expired tokens)iss claim matches expected issueraud claim includes this servicealg: none in productionSHOULD:
nbf claim (not before time)iat claim (issued at time)MUST NOT:
OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user accounts on an HTTP service. It works by delegating user authentication to the service that hosts the user account and authorizing third-party applications to access that account.
Specification: RFC 6749
Use case: Web applications with server-side backend
Flow:
+----------+
| Resource |
| Owner |
| |
+----------+
v
|
(A) User authorizes client
|
v
+-----------+ Client Identifier +---------------+
| +----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent +<---(B)-- Authorization Code ----+ Server |
| | | |
+-----------+ Authorization Code +---------------+
& Redirection URI
| ^ v
| | (C)
(B) (D)
v |
+-----------+ +---------------+
| +---(C)-- Authorization Code ----->| |
| Client | & Client Credentials | Token |
| +<---(D)----- Access Token --------+ Endpoint |
| | (& Refresh Token) | |
+-----------+ +---------------+
Request Parameters:
response_type=code (required)client_id (required)redirect_uri (required)scope (optional)state (recommended for CSRF protection)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://client.example.com/callback&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET
Use case: Server-to-server authentication (no user involved)
Request:
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 write
Use case: Obtain new access token without re-authentication
Request:
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_SECRET
{
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read write"
}
Bearer Tokens (RFC 6750):
GET /api/resource HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
{
"error": "invalid_request",
"error_description": "The redirect_uri is missing",
"error_uri": "https://docs.example.com/errors/invalid_request"
}
Error Codes:
invalid_request: Malformed requestinvalid_client: Client authentication failedinvalid_grant: Authorization code/refresh token invalidunauthorized_client: Client not authorized for this grant typeunsupported_grant_type: Grant type not supportedinvalid_scope: Requested scope invalid or exceeds granted scopeProof Key for Code Exchange (PKCE) is an extension to OAuth 2.0 that prevents authorization code interception attacks for public clients (mobile apps, SPAs).
Specification: RFC 7636
1. Client generates code verifier:
code_verifier = high-entropy random string (43-128 characters)
Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
2. Client creates code challenge:
code_challenge = BASE64URL(SHA256(code_verifier))
OR (plain method, less secure):
code_challenge = code_verifier
3. Authorization request includes code challenge:
GET /authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=https://app.example.com/callback&
scope=read&
state=xyz&
code_challenge=CHALLENGE&
code_challenge_method=S256
4. Token request includes code verifier:
POST /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&
code_verifier=VERIFIER
5. Server verifies:
BASE64URL(SHA256(code_verifier)) == code_challenge
MUST use:
SHOULD use:
| Storage | Security | Accessibility | Persistence | Recommendation | |---------|----------|---------------|-------------|----------------| | httpOnly Cookie | HIGH | Automatic | Yes | RECOMMENDED | | Secure Cookie | MEDIUM | Manual | Yes | Use with CSRF protection | | localStorage | LOW | High | Yes | AVOID (XSS vulnerable) | | sessionStorage | LOW | High | Tab-scoped | AVOID (XSS vulnerable) | | Memory (state) | HIGH | High | No | RECOMMENDED for SPAs |
Set token in httpOnly cookie:
Set-Cookie: access_token=JWT_TOKEN;
HttpOnly;
Secure;
SameSite=Strict;
Max-Age=3600;
Path=/
Cookie attributes:
HttpOnly: Prevents JavaScript access (XSS protection)Secure: HTTPS onlySameSite=Strict: Prevents CSRF attacksMax-Age: Expiration in secondsPath=/: Cookie scopeiOS (Keychain):
import Security
func saveToken(_ token: String) {
let data = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "access_token",
kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)
}
Android (EncryptedSharedPreferences):
import androidx.security.crypto.EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPreferences.edit().putString("access_token", token).apply()
Database schema for refresh tokens:
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INT NOT NULL REFERENCES users(id),
token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA-256 hash
family_id UUID NOT NULL, -- For token rotation detection
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
last_used_at TIMESTAMP,
ip_address INET,
user_agent TEXT,
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash);
CREATE INDEX idx_refresh_tokens_family ON refresh_tokens(family_id);
1. Algorithm Confusion Attack
Attack: Change alg header from RS256 to HS256, sign with public key
Mitigation:
# ALWAYS specify allowed algorithms explicitly
jwt.decode(token, key, algorithms=["RS256"]) # Good
jwt.decode(token, key, algorithms=["HS256", "RS256"]) # Bad
2. None Algorithm Attack
Attack: Set alg: none and remove signature
Mitigation:
# NEVER allow 'none' algorithm
jwt.decode(token, key, algorithms=["HS256"]) # 'none' rejected
3. Weak Secret Keys
Attack: Brute force weak HMAC secrets
Mitigation:
import secrets
# Generate cryptographically secure key (256 bits minimum)
secret = secrets.token_urlsafe(32) # 32 bytes = 256 bits
1. Authorization Code Interception
Attack: Intercept authorization code in redirect
Mitigation: Use PKCE (RFC 7636)
2. Redirect URI Manipulation
Attack: Change redirect_uri to attacker-controlled domain
Mitigation: Exact string matching on redirect URIs
ALLOWED_REDIRECTS = [
"https://app.example.com/callback",
"https://app.example.com/oauth/callback"
]
if redirect_uri not in ALLOWED_REDIRECTS:
raise ValueError("Invalid redirect URI")
3. State Parameter Missing
Attack: CSRF attack on OAuth flow
Mitigation: Always use state parameter
import secrets
# Generate random state
state = secrets.token_urlsafe(32)
# Store in session
session['oauth_state'] = state
# Validate in callback
if request.args.get('state') != session.get('oauth_state'):
raise ValueError("Invalid state parameter")
Attack: Inject JavaScript to steal tokens from localStorage
Vulnerable:
// Token accessible to JavaScript
localStorage.setItem('token', accessToken);
const stolen = localStorage.getItem('token');
Mitigation:
// Use httpOnly cookies (not accessible to JavaScript)
// OR store in memory only
let accessToken = null; // Lost on page refresh
Attack: Trick user into making authenticated requests
Vulnerable:
POST /api/transfer HTTP/1.1
Cookie: session_id=abc123
Content-Type: application/json
{"to": "attacker", "amount": 1000}
Mitigation:
# Use SameSite cookies
response.set_cookie(
'access_token',
token,
samesite='Strict', # or 'Lax'
httponly=True,
secure=True
)
# OR use CSRF tokens
csrf_token = secrets.token_urlsafe(32)
session['csrf_token'] = csrf_token
Attack: Reuse stolen/intercepted token
Mitigation:
Attack: Inject malicious claims into payload
Vulnerable:
# User-controlled data in token
token = jwt.encode({"role": request.json['role']}, secret)
Mitigation:
# Validate and sanitize inputs
allowed_roles = ['user', 'admin', 'moderator']
role = request.json['role'] if request.json['role'] in allowed_roles else 'user'
token = jwt.encode({"role": role}, secret)
Attack: Force user to use attacker's session ID
Mitigation:
# Regenerate session ID after login
from flask import session
@app.route('/login', methods=['POST'])
def login():
# Authenticate user
user = authenticate(request.json['email'], request.json['password'])
# Regenerate session ID
session.clear()
session.regenerate()
# Set new session data
session['user_id'] = user.id
| Algorithm | Year | Security | Speed | Memory | Recommendation | |-----------|------|----------|-------|--------|----------------| | MD5 | 1992 | BROKEN | Fast | Low | NEVER USE | | SHA-1 | 1995 | BROKEN | Fast | Low | NEVER USE | | SHA-256 | 2001 | Weak | Fast | Low | NOT FOR PASSWORDS | | bcrypt | 1999 | GOOD | Tunable | Low | RECOMMENDED | | scrypt | 2009 | BETTER | Tunable | Tunable | RECOMMENDED | | Argon2 | 2015 | BEST | Tunable | Tunable | HIGHLY RECOMMENDED | | PBKDF2 | 2000 | OK | Tunable | Low | ACCEPTABLE |
Variants:
Parameters:
time_cost: Number of iterations (3-10)memory_cost: Memory in KiB (65536 = 64MB)parallelism: Number of threads (4-8)Implementation:
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # Iterations
memory_cost=65536, # 64 MB
parallelism=4, # 4 threads
hash_len=32, # 32 bytes output
salt_len=16 # 16 bytes salt
)
# Hash password
hash = ph.hash("user_password")
# Verify password
try:
ph.verify(hash, "user_password")
print("Password valid")
except:
print("Password invalid")
# Check if rehashing needed (params changed)
if ph.check_needs_rehash(hash):
hash = ph.hash("user_password")
Cost factor: 2^cost iterations (10-14 recommended, 12 is good default)
Implementation:
import bcrypt
# Hash password
password = b"user_password"
salt = bcrypt.gensalt(rounds=12) # 2^12 = 4096 iterations
hash = bcrypt.hashpw(password, salt)
# Verify password
if bcrypt.checkpw(password, hash):
print("Password valid")
Target: 250-500ms hashing time on your hardware
import time
from argon2 import PasswordHasher
def benchmark_hashing(algorithm, password, iterations=100):
start = time.time()
for _ in range(iterations):
if algorithm == 'argon2':
ph = PasswordHasher()
hash = ph.hash(password)
elif algorithm == 'bcrypt':
import bcrypt
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
elapsed = time.time() - start
avg_time = (elapsed / iterations) * 1000 # ms
print(f"{algorithm}: {avg_time:.2f}ms per hash")
benchmark_hashing('argon2', 'test_password')
benchmark_hashing('bcrypt', 'test_password')
Last Updated: 2025-10-27 Version: 1.0