| name | api-authentication |
| description | This skill should be used when the user asks about "API authentication", "API authorization", "JWT", "JSON Web Token", "access token", "refresh token", "token rotation", "OAuth 2.0", "OAuth flow", "authorization code flow", "client credentials", "PKCE", "API key", "Bearer token", "session authentication", "cookie authentication", "httpOnly cookie", "CSRF", "CORS with credentials", "token expiry", "token refresh", "invalidating tokens", "logout", "revocation", "stateless auth", "stateful auth", "auth middleware", "validate JWT", "sign JWT", "decode JWT". Also trigger for "how do I protect my API routes", "how do I implement login", "should I use JWT or sessions", "how to handle token expiry", or "users being logged out too often". |
API Authentication
Production-ready patterns for authenticating and authorizing API consumers.
Authentication vs Authorization
Authentication (AuthN) — Who are you?
→ Verify identity using a credential (password, token, certificate)
→ "You are alice@example.com"
Authorization (AuthZ) — What can you do?
→ Enforce permissions for the verified identity
→ "alice can read orders but not refund them"
Both must be present. Authentication without authorization is incomplete.
Choosing an Auth Strategy
| Use case | Recommended | Why |
|---|
| Browser SPA + first-party API | HttpOnly cookies | XSS-safe; CSRF mitigable |
| Mobile app + first-party API | JWT in secure storage | No cookie jar on mobile |
| Server-to-server (machine) | OAuth 2.0 Client Credentials | No user involved |
| Third-party API consumers | API keys | Simple, revocable, auditable |
| "Login with Google/GitHub" | OAuth 2.0 Authorization Code + PKCE | Delegates identity to provider |
| Legacy / simple internal | Session + server-side store | Simple, fully revocable |
JWT (JSON Web Token)
Structure
header.payload.signature
eyJhbGciOiJIUzI1NiJ9 ← Base64({"alg":"HS256","typ":"JWT"})
.
eyJ1c2VyX2lkIjoiYWJjIn0 ← Base64({"user_id":"abc","exp":1234567890,"iat":1234567800})
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← HMAC-SHA256(header.payload, secret)
Signing and Verifying (Node.js)
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_ALGORITHM = 'HS256' as const;
const ACCESS_TOKEN_TTL = 15 * 60;
const REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60;
interface JWTPayload {
sub: string;
iat: number;
exp: number;
jti: string;
roles: string[];
}
function signAccessToken(userId: string, roles: string[]): string {
return jwt.sign(
{ sub: userId, roles, jti: crypto.randomUUID() },
JWT_SECRET,
{ algorithm: JWT_ALGORITHM, expiresIn: ACCESS_TOKEN_TTL }
);
}
function verifyToken(token: string): JWTPayload {
return jwt.verify(token, JWT_SECRET, {
algorithms: [JWT_ALGORITHM],
}) as JWTPayload;
}
function requireAuth(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: { code: 'MISSING_TOKEN' } });
}
try {
const payload = verifyToken(header.slice(7));
req.user = { id: payload.sub, roles: payload.roles };
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: { code: 'TOKEN_EXPIRED' } });
}
return res.status(401).json({ error: { code: 'INVALID_TOKEN' } });
}
}
Critical Security Rules
✓ Always specify algorithm in verify() call (prevents alg:none attacks)
✓ Use asymmetric keys (RS256/ES256) when multiple services verify tokens
✓ Keep access token TTL short (15 min). Use refresh tokens for longevity
✓ Include jti (JWT ID) for revocation capability
✓ Never put sensitive data in payload (it's base64, not encrypted)
✓ Validate iss (issuer) and aud (audience) claims for multi-service setups
✗ Never trust the alg field from the token header
✗ Never use alg:none
✗ Never store JWT secret in code — use environment variable
Refresh Token Rotation
Short access tokens (15 min) + long refresh tokens (7 days) = good security/UX balance.
interface RefreshToken {
id: string;
userId: string;
expiresAt: Date;
revokedAt: Date | null;
replacedByTokenId: string | null;
createdAt: Date;
}
async function refreshAccessToken(refreshToken: string) {
const storedToken = await db.refreshTokens.findUnique({
where: { id: refreshToken },
});
if (!storedToken) throw new UnauthorizedError('INVALID_REFRESH_TOKEN');
if (storedToken.revokedAt) {
await db.refreshTokens.revokeFamily(storedToken.userId);
throw new UnauthorizedError('REFRESH_TOKEN_REUSE');
}
if (storedToken.expiresAt < new Date()) {
throw new UnauthorizedError('REFRESH_TOKEN_EXPIRED');
}
const newRefreshToken = crypto.randomUUID() + crypto.randomUUID();
await db.$transaction([
db.refreshTokens.update({
where: { id: refreshToken },
data: { revokedAt: new Date(), replacedByTokenId: newRefreshToken },
}),
db.refreshTokens.create({
data: {
id: newRefreshToken,
userId: storedToken.userId,
expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL * 1000),
},
}),
]);
const user = await db.users.findById(storedToken.userId);
return {
accessToken: signAccessToken(user.id, user.roles),
refreshToken: newRefreshToken,
};
}
HttpOnly Cookie Auth (Browser SPAs)
Preferred over localStorage for browser applications — immune to XSS token theft.
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: ACCESS_TOKEN_TTL * 1000,
path: '/',
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: REFRESH_TOKEN_TTL * 1000,
path: '/api/auth/refresh',
});
function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) return res.status(401).json({ error: { code: 'MISSING_TOKEN' } });
}
app.post('/api/auth/logout', requireAuth, async (req, res) => {
await db.refreshTokens.revoke(req.cookies.refresh_token);
res.clearCookie('access_token', { httpOnly: true, secure: true, sameSite: 'lax' });
res.clearCookie('refresh_token', { httpOnly: true, secure: true, sameSite: 'lax', path: '/api/auth/refresh' });
res.status(204).end();
});
CSRF with cookies: Use sameSite: 'lax' for most apps. For sameSite: 'none' (cross-site), add a CSRF token (double-submit cookie or synchronizer token pattern).
API Keys
For third-party developers, service accounts, or CI/CD integrations.
function generateApiKey(prefix = 'sk_live'): { key: string; hash: string } {
const random = crypto.randomBytes(32).toString('base64url');
const key = `myapp_${prefix}_${random}`;
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash };
}
const { key, hash } = generateApiKey();
await db.apiKeys.create({ data: { keyHash: hash, userId, name, scopes } });
return { apiKey: key };
async function verifyApiKey(presentedKey: string) {
const hash = crypto.createHash('sha256').update(presentedKey).digest('hex');
const apiKey = await db.apiKeys.findUnique({
where: { keyHash: hash },
include: { user: true },
});
if (!apiKey || apiKey.revokedAt) throw new UnauthorizedError('INVALID_API_KEY');
db.apiKeys.update({ where: { keyHash: hash }, data: { lastUsedAt: new Date() } });
return apiKey;
}
OAuth 2.0 Authorization Code + PKCE
For "Login with Google/GitHub/etc." or exposing your API to third-party apps.
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
return { verifier, challenge };
}
app.get('/auth/github', (req, res) => {
const { verifier, challenge } = generatePKCE();
const state = crypto.randomBytes(16).toString('hex');
req.session.pkceVerifier = verifier;
req.session.oauthState = state;
const params = new URLSearchParams({
client_id: process.env.GITHUB_CLIENT_ID!,
redirect_uri: 'https://app.example.com/auth/github/callback',
scope: 'read:user user:email',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
res.redirect(`https://github.com/login/oauth/authorize?${params}`);
});
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(400).json({ error: { code: 'INVALID_STATE' } });
}
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: process.env.GITHUB_CLIENT_ID!,
client_secret: process.env.GITHUB_CLIENT_SECRET!,
code,
code_verifier: req.session.pkceVerifier,
}),
});
const { access_token } = await tokenResponse.json();
const profile = await fetchGitHubProfile(access_token);
const user = await findOrCreateUser({ email: profile.email, githubId: profile.id });
issueSessionCookies(res, user);
res.redirect('/dashboard');
});
Role-Based Authorization (RBAC)
const PERMISSIONS = {
'orders:read': ['customer', 'support', 'manager', 'admin'],
'orders:write': ['manager', 'admin'],
'orders:refund': ['manager', 'admin'],
'users:read': ['support', 'manager', 'admin'],
'users:write': ['admin'],
} as const;
type Permission = keyof typeof PERMISSIONS;
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user?.roles ?? [];
const allowedRoles = PERMISSIONS[permission];
if (!userRoles.some(role => allowedRoles.includes(role as any))) {
logger.warn({
user_id: req.user?.id,
required_permission: permission,
user_roles: userRoles,
path: req.path,
}, 'Authorization denied');
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
});
}
next();
};
}
app.post('/orders/:id/refund', requireAuth, requirePermission('orders:refund'), refundHandler);
Resource-Level Authorization (IDOR Prevention)
app.get('/orders/:id', requireAuth, async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order) return res.status(404).json({ error: { code: 'NOT_FOUND' } });
const canView = order.customerId === req.user.id
|| req.user.roles.some(r => ['admin', 'support'].includes(r));
if (!canView) {
return res.status(404).json({ error: { code: 'NOT_FOUND' } });
}
res.json(order);
});
Security Headers for APIs
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}));
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
app.use(cors({
origin: (origin, callback) => {
const allowed = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || allowed.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Idempotency-Key'],
}));
Deeper Reference
For complete token implementation patterns and OAuth flow diagrams, see:
references/token-patterns.md — JWT schema design, RS256 key rotation, JWKS endpoints, refresh token rotation algorithm, and opaque token patterns
references/oauth-flows.md — Authorization Code + PKCE, Client Credentials, Device Authorization flows with sequence diagrams and common mistake tables