9.2 KiB
9.2 KiB
Backend Authentication & Authorization
Modern authentication patterns including OAuth 2.1, JWT, RBAC, and MFA (2025 standards).
OAuth 2.1 (2025 Standard)
Key Changes from OAuth 2.0
Mandatory:
- PKCE (Proof Key for Code Exchange) for all clients
- Exact redirect URI matching
- State parameter for CSRF protection
Deprecated:
- Implicit grant flow (security risk)
- Resource owner password credentials grant
- Bearer token in query strings
Authorization Code Flow with PKCE
// Step 1: Generate code verifier and challenge
import crypto from 'crypto';
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
// Step 2: Redirect to authorization endpoint
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('client_id', 'your-client-id');
authUrl.searchParams.set('redirect_uri', 'https://app.example.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', crypto.randomBytes(16).toString('hex'));
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Step 3: Exchange code for token (with code_verifier)
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: codeVerifier,
}),
});
JWT (JSON Web Tokens)
Structure
Header.Payload.Signature
eyJhbGciOi... . eyJzdWIiOi... . SflKxwRJ...
Best Practices (2025)
- Short expiration - Access tokens: 15 minutes, Refresh tokens: 7 days
- Use RS256 - Asymmetric signing (not HS256 for public APIs)
- Validate everything - Signature, issuer, audience, expiration
- Include minimal claims - Don't include sensitive data
- Refresh token rotation - Issue new refresh token on each use
Implementation
import jwt from 'jsonwebtoken';
// Generate JWT
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
roles: user.roles,
},
process.env.JWT_PRIVATE_KEY,
{
algorithm: 'RS256',
expiresIn: '15m',
issuer: 'https://api.example.com',
audience: 'https://app.example.com',
}
);
// Verify JWT
const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
algorithms: ['RS256'],
issuer: 'https://api.example.com',
audience: 'https://app.example.com',
});
Role-Based Access Control (RBAC)
RBAC Model
Users → Roles → Permissions → Resources
Implementation (NestJS Example)
// Define roles
export enum Role {
ADMIN = 'admin',
EDITOR = 'editor',
VIEWER = 'viewer',
}
// Role decorator
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
// Guard implementation
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<Role[]>('roles', context.getHandler());
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const user = request.user;
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Usage
@Post()
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(Role.ADMIN, Role.EDITOR)
async createPost(@Body() createPostDto: CreatePostDto) {
return this.postsService.create(createPostDto);
}
RBAC Best Practices
- Deny by default - Explicitly grant permissions
- Least privilege - Minimum permissions needed
- Role hierarchy - Admin inherits Editor inherits Viewer
- Separate roles and permissions - Flexible permission assignment
- Audit trail - Log role changes and access
Multi-Factor Authentication (MFA)
TOTP (Time-Based One-Time Password)
import speakeasy from 'speakeasy';
import QRCode from 'qrcode';
// Generate secret
const secret = speakeasy.generateSecret({
name: 'MyApp',
issuer: 'MyCompany',
});
// Generate QR code for user
const qrCode = await QRCode.toDataURL(secret.otpauth_url);
// Verify TOTP token
const verified = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userToken,
window: 2, // Allow 2 time steps drift
});
FIDO2/WebAuthn (Passwordless - 2025 Standard)
Benefits:
- Phishing-resistant
- No shared secrets
- Hardware-backed security
- Better UX (biometrics, security keys)
Implementation:
// Registration
const publicKeyCredentialCreationOptions = {
challenge: crypto.randomBytes(32),
rp: { name: 'MyApp', id: 'example.com' },
user: {
id: Buffer.from(user.id),
name: user.email,
displayName: user.name,
},
pubKeyCredParams: [{ alg: -7, type: 'public-key' }], // ES256
authenticatorSelection: {
authenticatorAttachment: 'platform', // 'platform' or 'cross-platform'
userVerification: 'required',
},
timeout: 60000,
attestation: 'direct',
};
// Use @simplewebauthn/server library
import { verifyRegistrationResponse, verifyAuthenticationResponse } from '@simplewebauthn/server';
Session Management
Best Practices
- Secure cookies - HttpOnly, Secure, SameSite=Strict
- Session timeout - Idle: 15 minutes, Absolute: 8 hours
- Regenerate session ID - After login, privilege elevation
- Server-side storage - Redis for distributed systems
- CSRF protection - SameSite cookies + CSRF tokens
Implementation
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient();
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'strict', // CSRF protection
maxAge: 1000 * 60 * 15, // 15 minutes
},
})
);
Password Security
Argon2id (2025 Standard - Replaces bcrypt)
Why Argon2id:
- Winner of Password Hashing Competition (2015)
- Memory-hard (resistant to GPU/ASIC attacks)
- Configurable CPU and memory cost
- Combines Argon2i (data-independent) + Argon2d (data-dependent)
import argon2 from 'argon2';
// Hash password
const hash = await argon2.hash('password123', {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 4, // 4 threads
});
// Verify password
const valid = await argon2.verify(hash, 'password123');
Password Policy (2025 NIST Guidelines)
- Minimum length: 12 characters (not 8)
- No composition rules - Allow passphrases
- Check against breach databases - HaveIBeenPwned API
- No periodic rotation - Only on compromise
- Allow all printable characters - Including spaces, emojis
API Key Authentication
Best Practices
- Prefix keys -
sk_live_,pk_test_(identify type/environment) - Hash stored keys - Store SHA-256 hash, not plaintext
- Key rotation - Allow users to rotate keys
- Scope limiting - Separate keys for read/write operations
- Rate limiting - Per API key limits
// Generate API key
const apiKey = `sk_${env}_${crypto.randomBytes(24).toString('base64url')}`;
// Store hashed version
const hashedKey = crypto.createHash('sha256').update(apiKey).digest('hex');
await db.apiKeys.create({ userId, hashedKey, scopes: ['read'] });
// Validate API key
const providedHash = crypto.createHash('sha256').update(providedKey).digest('hex');
const keyRecord = await db.apiKeys.findOne({ hashedKey: providedHash });
Authentication Decision Matrix
| Use Case | Recommended Approach |
|---|---|
| Web application | OAuth 2.1 + JWT |
| Mobile app | OAuth 2.1 + PKCE |
| SPA (Single Page App) | OAuth 2.1 Authorization Code + PKCE |
| Server-to-server | Client credentials grant + mTLS |
| Third-party API access | API keys with scopes |
| High-security | WebAuthn/FIDO2 + MFA |
| Internal admin | JWT + RBAC + MFA |
| Microservices | Service mesh (mTLS) + JWT |
Security Checklist
- OAuth 2.1 with PKCE implemented
- JWT tokens expire in 15 minutes
- Refresh token rotation enabled
- RBAC with deny-by-default
- MFA required for admin accounts
- Passwords hashed with Argon2id
- Session cookies: HttpOnly, Secure, SameSite
- Rate limiting on auth endpoints (10 attempts/15 min)
- Account lockout after failed attempts
- Password policy: 12+ chars, breach check
- Audit logging for authentication events
Resources
- OAuth 2.1: https://oauth.net/2.1/
- JWT Best Practices: https://datatracker.ietf.org/doc/html/rfc8725
- WebAuthn: https://webauthn.guide/
- NIST Password Guidelines: https://pages.nist.gov/800-63-3/
- OWASP Auth Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html