A practical guide to API authentication and authorization, including OAuth 2.0, JWT, and API key management.
OAuth 2.0 is the most widely used authorization framework for API access. Selecting the appropriate grant type is critical.
| Grant Type | Use Case | Security Level |
|---|---|---|
| Authorization Code + PKCE | SPAs and mobile apps (recommended) | Highest |
| Authorization Code | Server-side web apps | High |
| Client Credentials | Server-to-server communication (M2M) | Medium |
| Implicit (deprecated) | Legacy SPAs | Low |
| Resource Owner Password (deprecated) | Highly trusted first-party only | Low |
Implicit Grant is now deprecated because the token is exposed in the URL fragment. Use Authorization Code + PKCE for SPAs.
// 1. Randomly generate code_verifier function generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 2. Generate code_challenge with SHA-256 async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } // 3. Authorization request const verifier = generateCodeVerifier(); const challenge = await generateCodeChallenge(verifier); const authUrl = `https://auth.example.com/authorize?` + `response_type=code&` + `client_id=${clientId}&` + `redirect_uri=${redirectUri}&` + `code_challenge=${challenge}&` + `code_challenge_method=S256&` + `scope=read write`;
JWT is widely used as a stateless authentication token, but implementation mistakes directly lead to security vulnerabilities.
const jwt = require('jsonwebtoken'); // --- Token Issuance --- function issueTokens(user) { const accessToken = jwt.sign( { sub: user.id, role: user.role }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '15m', issuer: 'api.example.com', audience: 'app.example.com', } ); const refreshToken = jwt.sign( { sub: user.id, type: 'refresh' }, process.env.REFRESH_SECRET, { algorithm: 'HS256', expiresIn: '7d' } ); return { accessToken, refreshToken }; } // --- Token Verification --- function verifyAccessToken(token) { return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'], // ← Always specify! issuer: 'api.example.com', audience: 'app.example.com', }); }
| Storage Location | XSS Resistance | CSRF Resistance | Recommendation |
|---|---|---|---|
| HttpOnly Cookie | ◎ | △ (requires SameSite setting) | Recommended |
| Memory (variable) | ◎ | ◎ | Lost on page navigation |
| localStorage | ✕ (can be stolen via XSS) | ◎ | Not recommended |
| sessionStorage | ✕ (can be stolen via XSS) | ◎ | Limited use |
Storing the access token in a cookie with HttpOnly, Secure, and SameSite=Strict attributes is the safest approach. This prevents token theft through XSS attacks.
API keys are a simple authentication mechanism, but improper management can lead to serious risks.
Generate with sufficient entropy (256 bits or more). Store hashed; never keep plain text in the database.
Assign minimum scopes per key (read-only, specific resources only, etc.). Avoid wildcard permissions.
Rotate keys regularly (within 90 days). Set a grace period for old keys. Revoke immediately upon leakage.
Send via header (X-API-Key or Authorization). Do not include in URL query parameters (they appear in logs).
Do not hard-code keys in source code. Manage them with environment variables or a secret manager.
Monitor API key usage. Detect anomalous patterns (mass requests, access from unknown IPs).
const crypto = require('crypto'); // Hash the API key with SHA-256 and compare async function validateApiKey(req, res, next) { const apiKey = req.headers['x-api-key']; if (!apiKey) { return res.status(401).json({ error: 'API key required' }); } // Hash and look up in DB (timingSafeEqual not needed since we compare hashes) const hashedKey = crypto .createHash('sha256') .update(apiKey) .digest('hex'); const keyRecord = await db.findApiKey(hashedKey); if (!keyRecord || keyRecord.revokedAt) { return res.status(403).json({ error: 'Invalid API key' }); } // Check expiration if (keyRecord.expiresAt && new Date() > keyRecord.expiresAt) { return res.status(403).json({ error: 'API key expired' }); } req.apiClient = keyRecord; next(); }
AI agents and LLM-powered applications require additional authentication and authorization patterns beyond traditional API security.
Assign different access levels per model tier (e.g., GPT-4 requires elevated scope). Prevent unauthorized use of expensive or sensitive models.
Embed token budgets in access tokens or API key metadata. Enforce per-request and per-day limits to prevent denial-of-wallet attacks.
Check user permissions before executing certain prompt types (e.g., code generation, data analysis). Map prompt categories to role-based permissions.
Ensure conversation history, fine-tuned models, and RAG data are isolated per tenant. Use tenant-scoped API keys with namespace enforcement.
Assign cryptographic identities to AI agents. Use signed JWTs for inter-agent communication. Verify agent identity before granting tool access.
Automate API key rotation for LLM providers. Use short-lived tokens for agent sessions. Revoke credentials immediately when agents are decommissioned.
| Scenario | Auth Method | Required Scope | Approval |
|---|---|---|---|
| Agent reads public data | API Key | data:read |
Automatic |
| Agent modifies user data | OAuth 2.0 (delegated) | data:write |
User consent required |
| Agent calls external API | OAuth 2.0 + mTLS | external:invoke |
Human-in-the-loop |
| Agent executes code / shell | Scoped JWT + Sandbox | exec:sandbox |
Admin approval + audit log |
import jwt import time from datetime import datetime, timedelta class AgentTokenIssuer: """Issues scoped, short-lived tokens for AI agents.""" ALLOWED_SCOPES = { "reader": ["data:read"], "writer": ["data:read", "data:write"], "executor": ["data:read", "data:write", "exec:sandbox"], } def __init__(self, secret_key: str): self.secret_key = secret_key def issue_agent_token( self, agent_id: str, role: str, tenant_id: str, token_budget: int = 10000, ttl_minutes: int = 15, ) -> str: # Validate role and resolve scopes if role not in self.ALLOWED_SCOPES: raise ValueError(f"Invalid role: {role}") payload = { "sub": agent_id, "tenant": tenant_id, "scopes": self.ALLOWED_SCOPES[role], "token_budget": token_budget, "iat": datetime.utcnow(), "exp": datetime.utcnow() + timedelta(minutes=ttl_minutes), "type": "agent", } return jwt.encode(payload, self.secret_key, algorithm="HS256") def verify_tool_access(self, token: str, required_scope: str) -> dict: # Decode and verify agent token payload = jwt.decode( token, self.secret_key, algorithms=["HS256"] ) if required_scope not in payload["scopes"]: raise PermissionError( f"Agent {payload['sub']} lacks scope: {required_scope}" ) return payload
Related: LLM06: Excessive Agency, ASI01: Excessive Agency, ASI03: Insecure Tool/Function Calling
| Method | Stateless | Use Case | Security Considerations |
|---|---|---|---|
| OAuth 2.0 + PKCE | ○ | APIs requiring user authorization | state/PKCE required, token expiration management |
| JWT Bearer | ○ | Inter-microservice communication | Algorithm specification required, no sensitive data in payload |
| API Key | ○ | Server-to-server, external integrations | Rotation and scope restriction required |
| mTLS | ○ | High-security M2M communication | High operational cost for certificate management |
| Session Cookie | ✕ | Traditional web applications | CSRF protection required, scalability challenges |