API authentication patterns including JWT, OAuth 2.0, API keys, and session-based auth. Covers token generation, validation, refresh strategies, security best practices, and when to use each pattern. Use when implementing API authentication, choosing auth strategy, securing endpoints, or debugging auth issues. Prevents common vulnerabilities like token theft, replay attacks, and insecure storage.
Add this skill
npx mdskills install applied-artificial-intelligence/api-authenticationComprehensive authentication guide with decision matrix, security best practices, and working code examples
1---2name: api-authentication3description: API authentication patterns including JWT, OAuth 2.0, API keys, and session-based auth. Covers token generation, validation, refresh strategies, security best practices, and when to use each pattern. Use when implementing API authentication, choosing auth strategy, securing endpoints, or debugging auth issues. Prevents common vulnerabilities like token theft, replay attacks, and insecure storage.4---56# API Authentication Patterns78Comprehensive guide to implementing secure API authentication including JWT, OAuth 2.0, API keys, and session-based patterns. Covers when to use each approach, security best practices, and common vulnerabilities to avoid.910---1112## Quick Reference1314**When to use this skill:**15- Implementing API authentication16- Choosing between auth strategies (JWT vs OAuth vs sessions)17- Securing API endpoints18- Implementing token refresh logic19- Debugging authentication issues20- Preventing auth vulnerabilities2122**Common triggers:**23- "How should I implement authentication"24- "JWT vs OAuth vs API keys"25- "How to secure this API"26- "Implement refresh tokens"27- "Store authentication tokens securely"28- "Fix authentication vulnerability"2930**Prevents vulnerabilities:**31- Token theft and replay attacks32- Insecure token storage33- Missing token expiration34- Weak password hashing35- CSRF attacks3637---3839## Part 1: Authentication Strategy Decision Matrix4041### When to Use Each Pattern4243| Pattern | Best For | Pros | Cons |44|---------|----------|------|------|45| **JWT** | Stateless APIs, microservices, mobile apps | Stateless, scalable, works across domains | Tokens can't be revoked easily, larger payload |46| **OAuth 2.0** | Third-party access, social login, delegation | Industry standard, fine-grained permissions | Complex to implement, requires authorization server |47| **API Keys** | Server-to-server, public APIs, rate limiting | Simple, great for service accounts | Not for users, can't be scoped easily |48| **Sessions** | Traditional web apps, SSR, same-domain | Revocable, server-controlled, secure | Requires server state, doesn't scale horizontally easily |4950### Decision Tree5152```53START: What type of client?5455├─ Mobile app or SPA?56│ └─ Use JWT (stateless, works across domains)57│58├─ Third-party integration?59│ └─ Use OAuth 2.0 (delegation, scoped permissions)60│61├─ Service-to-service?62│ └─ Use API Keys (simple, rate-limitable)63│64└─ Traditional web app (same domain)?65 └─ Use Sessions (revocable, server-controlled)66```6768---6970## Part 2: JWT (JSON Web Tokens)7172### JWT Structure7374```75eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c7677[HEADER].[PAYLOAD].[SIGNATURE]78```7980**Header** (algorithm and type):81```json82{83 "alg": "HS256",84 "typ": "JWT"85}86```8788**Payload** (claims):89```json90{91 "sub": "1234567890", // Subject (user ID)92 "name": "John Doe", // Custom claim93 "iat": 1516239022, // Issued at94 "exp": 1516242622 // Expires at (required!)95}96```9798**Signature** (verification):99```100HMACSHA256(101 base64UrlEncode(header) + "." + base64UrlEncode(payload),102 secret103)104```105106### JWT Implementation (Python)107108```python109import jwt110import datetime111from fastapi import HTTPException, Security112from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials113114SECRET_KEY = "your-256-bit-secret" # Must be strong, from environment115ALGORITHM = "HS256"116ACCESS_TOKEN_EXPIRE_MINUTES = 15117REFRESH_TOKEN_EXPIRE_DAYS = 7118119security = HTTPBearer()120121def create_access_token(user_id: int) -> str:122 """Create short-lived access token."""123 expires = datetime.datetime.utcnow() + datetime.timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)124 payload = {125 "sub": str(user_id),126 "exp": expires,127 "iat": datetime.datetime.utcnow(),128 "type": "access"129 }130 return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)131132def create_refresh_token(user_id: int) -> str:133 """Create long-lived refresh token."""134 expires = datetime.datetime.utcnow() + datetime.timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)135 payload = {136 "sub": str(user_id),137 "exp": expires,138 "iat": datetime.datetime.utcnow(),139 "type": "refresh"140 }141 return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)142143def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:144 """Verify and decode JWT token."""145 token = credentials.credentials146 try:147 payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])148 if payload.get("type") != "access":149 raise HTTPException(status_code=401, detail="Invalid token type")150 return payload151 except jwt.ExpiredSignatureError:152 raise HTTPException(status_code=401, detail="Token expired")153 except jwt.InvalidTokenError:154 raise HTTPException(status_code=401, detail="Invalid token")155156# Login endpoint157@app.post("/login")158async def login(username: str, password: str):159 user = authenticate_user(username, password) # Your auth logic160 if not user:161 raise HTTPException(status_code=401, detail="Invalid credentials")162163 access_token = create_access_token(user.id)164 refresh_token = create_refresh_token(user.id)165166 return {167 "access_token": access_token,168 "refresh_token": refresh_token,169 "token_type": "bearer"170 }171172# Protected endpoint173@app.get("/protected")174async def protected_route(payload: dict = Depends(verify_token)):175 user_id = payload["sub"]176 return {"message": f"Hello user {user_id}"}177178# Refresh token endpoint179@app.post("/refresh")180async def refresh(refresh_token: str):181 try:182 payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])183 if payload.get("type") != "refresh":184 raise HTTPException(status_code=401, detail="Invalid token type")185186 # Generate new access token187 user_id = int(payload["sub"])188 new_access_token = create_access_token(user_id)189190 return {"access_token": new_access_token, "token_type": "bearer"}191 except jwt.ExpiredSignatureError:192 raise HTTPException(status_code=401, detail="Refresh token expired")193 except jwt.InvalidTokenError:194 raise HTTPException(status_code=401, detail="Invalid refresh token")195```196197### JWT Security Best Practices198199**✅ Do:**200- Use strong secret keys (256-bit minimum)201- Always set expiration (`exp` claim)202- Use short-lived access tokens (15 minutes)203- Use separate refresh tokens (7 days)204- Store secret in environment variables205- Use HTTPS only206- Validate signature on every request207- Check token type (`access` vs `refresh`)208209**❌ Don't:**210- Store sensitive data in payload (it's base64, not encrypted!)211- Use symmetric signing (HS256) for public APIs (use RS256)212- Store tokens in localStorage (XSS vulnerability)213- Skip expiration validation214- Use same token for access and refresh215- Hard-code secrets216217### Token Storage (Client-Side)218219**❌ Bad** (localStorage - vulnerable to XSS):220```javascript221localStorage.setItem('token', token); // XSS can steal this!222```223224**✅ Good** (httpOnly cookie):225```python226# Server sets httpOnly cookie227response.set_cookie(228 key="access_token",229 value=access_token,230 httponly=True, # Not accessible via JavaScript231 secure=True, # HTTPS only232 samesite="lax", # CSRF protection233 max_age=900 # 15 minutes234)235```236237**✅ Also Good** (memory only for SPAs):238```javascript239// Store in memory (lost on refresh, but more secure)240let accessToken = null;241242async function login(username, password) {243 const response = await fetch('/login', {244 method: 'POST',245 body: JSON.stringify({ username, password })246 });247 const data = await response.json();248 accessToken = data.access_token; // Store in memory249}250```251252---253254## Part 3: OAuth 2.0255256### OAuth 2.0 Flows257258**Authorization Code Flow** (most common, for web apps):259```2601. Client → Authorization Server: "User wants to log in"2612. Authorization Server → User: Login page2623. User → Authorization Server: Credentials2634. Authorization Server → Client: Authorization code2645. Client → Authorization Server: Exchange code for access token2656. Authorization Server → Client: Access token + refresh token266```267268**Client Credentials Flow** (for service-to-service):269```2701. Service → Authorization Server: Client ID + Secret2712. Authorization Server → Service: Access token272```273274### OAuth 2.0 Implementation (Authorization Code Flow)275276```python277from fastapi import FastAPI, HTTPException278from authlib.integrations.starlette_client import OAuth279import os280281app = FastAPI()282283oauth = OAuth()284oauth.register(285 name='google',286 client_id=os.getenv('GOOGLE_CLIENT_ID'),287 client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),288 server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',289 client_kwargs={'scope': 'openid email profile'}290)291292@app.get('/login/google')293async def login_google(request: Request):294 redirect_uri = request.url_for('auth_google')295 return await oauth.google.authorize_redirect(request, redirect_uri)296297@app.get('/auth/google')298async def auth_google(request: Request):299 try:300 token = await oauth.google.authorize_access_token(request)301 user_info = token.get('userinfo')302303 # Create or update user in your database304 user = get_or_create_user(305 email=user_info['email'],306 name=user_info['name']307 )308309 # Create your own JWT for subsequent requests310 access_token = create_access_token(user.id)311312 return {"access_token": access_token, "token_type": "bearer"}313 except Exception as e:314 raise HTTPException(status_code=400, detail=str(e))315```316317### OAuth 2.0 Scopes318319```python320# Define scopes for your API321SCOPES = {322 "read:posts": "Read posts",323 "write:posts": "Create and edit posts",324 "delete:posts": "Delete posts",325 "read:profile": "Read user profile",326 "write:profile": "Update user profile"327}328329# Include scopes in JWT330def create_access_token(user_id: int, scopes: list[str]) -> str:331 payload = {332 "sub": str(user_id),333 "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15),334 "scopes": scopes # Add scopes to token335 }336 return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)337338# Check scopes in protected endpoint339def require_scopes(required_scopes: list[str]):340 def decorator(func):341 async def wrapper(payload: dict = Depends(verify_token)):342 token_scopes = payload.get("scopes", [])343 if not all(scope in token_scopes for scope in required_scopes):344 raise HTTPException(status_code=403, detail="Insufficient permissions")345 return await func(payload)346 return wrapper347 return decorator348349@app.delete("/posts/{post_id}")350@require_scopes(["delete:posts"])351async def delete_post(post_id: int, payload: dict = Depends(verify_token)):352 # User has delete:posts scope353 pass354```355356---357358## Part 4: API Keys359360### API Key Implementation361362```python363import secrets364import hashlib365from datetime import datetime366367# Generate API key368def generate_api_key() -> tuple[str, str]:369 """Generate API key and return (key, hashed_key)."""370 # Generate random 32-byte key371 api_key = secrets.token_urlsafe(32)372373 # Hash for storage (never store plain key!)374 hashed_key = hashlib.sha256(api_key.encode()).hexdigest()375376 return api_key, hashed_key377378# Store in database379def create_api_key(user_id: int, name: str) -> str:380 api_key, hashed_key = generate_api_key()381382 db.execute("""383 INSERT INTO api_keys (user_id, name, key_hash, created_at)384 VALUES (?, ?, ?, ?)385 """, user_id, name, hashed_key, datetime.utcnow())386387 # Return plain key to user (only time they see it!)388 return api_key389390# Verify API key391def verify_api_key(api_key: str) -> dict:392 """Verify API key and return user info."""393 hashed_key = hashlib.sha256(api_key.encode()).hexdigest()394395 result = db.execute("""396 SELECT user_id, name, created_at, last_used_at397 FROM api_keys398 WHERE key_hash = ? AND revoked_at IS NULL399 """, hashed_key).fetchone()400401 if not result:402 raise HTTPException(status_code=401, detail="Invalid API key")403404 # Update last used timestamp405 db.execute("""406 UPDATE api_keys407 SET last_used_at = ?408 WHERE key_hash = ?409 """, datetime.utcnow(), hashed_key)410411 return {"user_id": result[0], "key_name": result[1]}412413# Middleware414from fastapi.security import APIKeyHeader415416api_key_header = APIKeyHeader(name="X-API-Key")417418@app.get("/api/data")419async def get_data(api_key: str = Security(api_key_header)):420 user_info = verify_api_key(api_key)421 return {"data": "protected data", "user_id": user_info["user_id"]}422```423424### API Key Best Practices425426**✅ Do:**427- Hash keys before storing (use SHA-256 minimum)428- Generate cryptographically secure keys (`secrets` module)429- Allow users to name keys ("Production Server", "CI/CD")430- Track last used timestamp431- Allow key revocation432- Rate limit by API key433- Log API key usage434435**❌ Don't:**436- Store plain text keys437- Use predictable key generation438- Expose keys in URLs (use headers)439- Share keys across environments440441### API Key Revocation442443```python444@app.delete("/api-keys/{key_id}")445async def revoke_api_key(key_id: int, current_user: dict = Depends(get_current_user)):446 db.execute("""447 UPDATE api_keys448 SET revoked_at = ?449 WHERE id = ? AND user_id = ?450 """, datetime.utcnow(), key_id, current_user["id"])451452 return {"message": "API key revoked"}453```454455---456457## Part 5: Session-Based Authentication458459### Session Implementation460461```python462from fastapi import FastAPI, Cookie, Response463import redis464import secrets465import json466467app = FastAPI()468redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)469470def create_session(user_id: int) -> str:471 """Create session and return session ID."""472 session_id = secrets.token_urlsafe(32)473474 session_data = {475 "user_id": user_id,476 "created_at": datetime.utcnow().isoformat()477 }478479 # Store in Redis with 24-hour expiry480 redis_client.setex(481 f"session:{session_id}",482 86400, # 24 hours483 json.dumps(session_data)484 )485486 return session_id487488def verify_session(session_id: str) -> dict:489 """Verify session and return user data."""490 session_data = redis_client.get(f"session:{session_id}")491492 if not session_data:493 raise HTTPException(status_code=401, detail="Session expired")494495 return json.loads(session_data)496497@app.post("/login")498async def login(username: str, password: str, response: Response):499 user = authenticate_user(username, password)500 if not user:501 raise HTTPException(status_code=401, detail="Invalid credentials")502503 # Create session504 session_id = create_session(user.id)505506 # Set httpOnly cookie507 response.set_cookie(508 key="session_id",509 value=session_id,510 httponly=True,511 secure=True,512 samesite="lax",513 max_age=86400 # 24 hours514 )515516 return {"message": "Logged in successfully"}517518@app.get("/protected")519async def protected_route(session_id: str = Cookie(None)):520 if not session_id:521 raise HTTPException(status_code=401, detail="Not authenticated")522523 session_data = verify_session(session_id)524 user_id = session_data["user_id"]525526 return {"message": f"Hello user {user_id}"}527528@app.post("/logout")529async def logout(session_id: str = Cookie(None), response: Response):530 if session_id:531 redis_client.delete(f"session:{session_id}")532533 response.delete_cookie("session_id")534 return {"message": "Logged out successfully"}535```536537---538539## Part 6: Password Security540541### Password Hashing (Never Store Plain Text!)542543```python544import bcrypt545546def hash_password(password: str) -> str:547 """Hash password with bcrypt."""548 salt = bcrypt.gensalt(rounds=12) # 12 rounds is good balance549 hashed = bcrypt.hashpw(password.encode('utf-8'), salt)550 return hashed.decode('utf-8')551552def verify_password(password: str, hashed: str) -> bool:553 """Verify password against hash."""554 return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))555556# When creating user557@app.post("/register")558async def register(username: str, password: str):559 # Validate password strength560 if len(password) < 12:561 raise HTTPException(status_code=400, detail="Password must be at least 12 characters")562563 # Hash password564 hashed_password = hash_password(password)565566 # Store hashed password (NEVER plain text!)567 db.execute("""568 INSERT INTO users (username, password_hash)569 VALUES (?, ?)570 """, username, hashed_password)571572 return {"message": "User created"}573574# When logging in575@app.post("/login")576async def login(username: str, password: str):577 user = db.execute("SELECT id, password_hash FROM users WHERE username = ?", username).fetchone()578579 if not user:580 raise HTTPException(status_code=401, detail="Invalid credentials")581582 # Verify password583 if not verify_password(password, user[1]):584 raise HTTPException(status_code=401, detail="Invalid credentials")585586 # Create token/session587 access_token = create_access_token(user[0])588 return {"access_token": access_token}589```590591### Password Requirements592593**Minimum requirements**:594- At least 12 characters (NIST recommendation)595- Mix of uppercase, lowercase, numbers, symbols596- Not in common password list597- Not similar to username598599**Implementation**:600```python601import re602603def validate_password(password: str, username: str) -> tuple[bool, str]:604 """Validate password strength."""605 if len(password) < 12:606 return False, "Password must be at least 12 characters"607608 if not re.search(r"[a-z]", password):609 return False, "Password must contain lowercase letter"610611 if not re.search(r"[A-Z]", password):612 return False, "Password must contain uppercase letter"613614 if not re.search(r"\d", password):615 return False, "Password must contain number"616617 if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):618 return False, "Password must contain special character"619620 if username.lower() in password.lower():621 return False, "Password cannot contain username"622623 # Check against common passwords624 if is_common_password(password):625 return False, "Password is too common"626627 return True, "Password is strong"628```629630---631632## Part 7: Common Vulnerabilities633634### Vulnerability 1: Token Replay Attacks635636**Problem**: Attacker intercepts token and reuses it637638**✅ Solution**: Short expiration + refresh tokens639```python640# Access token: 15 minutes641# Refresh token: 7 days, single-use642643def refresh_tokens(refresh_token: str):644 # Verify refresh token645 payload = jwt.decode(refresh_token, SECRET_KEY)646647 # Check if already used (store used tokens in Redis)648 if redis_client.get(f"used:{refresh_token}"):649 raise HTTPException(status_code=401, detail="Token already used")650651 # Mark as used652 redis_client.setex(f"used:{refresh_token}", 604800, "1") # 7 days653654 # Generate new tokens655 user_id = int(payload["sub"])656 new_access = create_access_token(user_id)657 new_refresh = create_refresh_token(user_id)658659 return {"access_token": new_access, "refresh_token": new_refresh}660```661662### Vulnerability 2: CSRF Attacks663664**Problem**: Attacker tricks user into making authenticated request665666**✅ Solution**: CSRF tokens + SameSite cookies667```python668from fastapi import Cookie, Header669670def verify_csrf(671 csrf_token: str = Header(None, alias="X-CSRF-Token"),672 session_id: str = Cookie(None)673):674 """Verify CSRF token matches session."""675 if not csrf_token:676 raise HTTPException(status_code=403, detail="CSRF token missing")677678 session_data = verify_session(session_id)679 stored_csrf = session_data.get("csrf_token")680681 if csrf_token != stored_csrf:682 raise HTTPException(status_code=403, detail="Invalid CSRF token")683684@app.post("/sensitive-action")685async def sensitive_action(686 csrf_check: None = Depends(verify_csrf)687):688 # Action protected from CSRF689 pass690```691692### Vulnerability 3: Timing Attacks693694**Problem**: Attacker uses response timing to guess credentials695696**✅ Solution**: Constant-time comparison697```python698import hmac699700def constant_time_compare(a: str, b: str) -> bool:701 """Compare strings in constant time (prevents timing attacks)."""702 return hmac.compare_digest(a, b)703704# Use for password hash comparison705if not constant_time_compare(provided_hash, stored_hash):706 raise HTTPException(status_code=401)707```708709---710711## Part 8: Rate Limiting712713```python714from fastapi import Request715import time716717# In-memory rate limiter (use Redis for production)718rate_limits = {}719720def rate_limit(max_requests: int, window_seconds: int):721 """Rate limit decorator."""722 def decorator(func):723 async def wrapper(request: Request, *args, **kwargs):724 client_ip = request.client.host725 key = f"{client_ip}:{func.__name__}"726727 now = time.time()728729 if key not in rate_limits:730 rate_limits[key] = []731732 # Remove old requests outside window733 rate_limits[key] = [734 req_time for req_time in rate_limits[key]735 if now - req_time < window_seconds736 ]737738 # Check if over limit739 if len(rate_limits[key]) >= max_requests:740 raise HTTPException(741 status_code=429,742 detail=f"Rate limit exceeded. Try again in {window_seconds} seconds."743 )744745 # Add current request746 rate_limits[key].append(now)747748 return await func(request, *args, **kwargs)749 return wrapper750 return decorator751752@app.post("/login")753@rate_limit(max_requests=5, window_seconds=60) # 5 login attempts per minute754async def login(request: Request, username: str, password: str):755 # Login logic756 pass757```758759---760761## Quick Security Checklist762763**Token Security**:764- [ ] Always use HTTPS (never HTTP)765- [ ] Set token expiration (15 min for access, 7 days for refresh)766- [ ] Store secrets in environment variables767- [ ] Hash API keys before storing768- [ ] Use httpOnly cookies for tokens769- [ ] Never store sensitive data in JWT payload770771**Password Security**:772- [ ] Use bcrypt or argon2 for hashing773- [ ] Enforce minimum 12 characters774- [ ] Never store plain text passwords775- [ ] Use constant-time comparison776- [ ] Implement rate limiting on login777778**API Security**:779- [ ] Validate all inputs780- [ ] Implement rate limiting781- [ ] Use CORS restrictions782- [ ] Add CSRF protection for state-changing operations783- [ ] Log authentication events784- [ ] Monitor for suspicious patterns785786---787788## Resources789790**JWT**:791- JWT.io: https://jwt.io/792- RFC 7519: https://tools.ietf.org/html/rfc7519793794**OAuth 2.0**:795- OAuth 2.0 RFC: https://tools.ietf.org/html/rfc6749796- OAuth 2.0 Playground: https://www.oauth.com/playground/797798**Security**:799- OWASP Auth Cheatsheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html800- NIST Password Guidelines: https://pages.nist.gov/800-63-3/801802**Libraries**:803- PyJWT: https://pyjwt.readthedocs.io/804- Authlib: https://docs.authlib.org/805- Passlib: https://passlib.readthedocs.io/806
Full transparency — inspect the skill content before installing.