Authentication and Authorization in Production Systems
Implement secure JWT authentication with refresh token rotation, RBAC, and OAuth 2.0 flows. Production patterns from healthcare and government systems.
Introduction
Authentication (who are you?) and authorization (what can you do?) are the foundation of application security. Getting them wrong exposes your users and data to significant risk.
After building auth systems for healthcare platforms handling PHI, government systems with classified data, and financial applications, I've developed a comprehensive approach that balances security with usability.
Authentication: Proving Identity
JWT Best Practices
// JWT configuration that follows security best practices interface JWTConfig { accessTokenExpiry: string; // Short-lived: 15-30 minutes refreshTokenExpiry: string; // Longer: 7-30 days algorithm: 'RS256' | 'ES256'; // Asymmetric algorithms for production issuer: string; audience: string; } class TokenService { private readonly privateKey: string; private readonly publicKey: string; constructor(private config: JWTConfig) { this.privateKey = fs.readFileSync('./keys/private.pem', 'utf8'); this.publicKey = fs.readFileSync('./keys/public.pem', 'utf8'); } generateAccessToken(user: User, sessionId: string): string { return jwt.sign( { sub: user.id, email: user.email, roles: user.roles, sessionId, type: 'access', }, this.privateKey, { algorithm: this.config.algorithm, expiresIn: this.config.accessTokenExpiry, issuer: this.config.issuer, audience: this.config.audience, jwtid: crypto.randomUUID(), // Unique token ID for revocation } ); } generateRefreshToken(userId: string, sessionId: string): string { const token = crypto.randomBytes(64).toString('base64url'); // Store refresh token hash in database // Never store the actual token const tokenHash = this.hashToken(token); await this.refreshTokenRepository.create({ userId, sessionId, tokenHash, expiresAt: addDays(new Date(), 30), createdAt: new Date(), }); return token; } async verifyAccessToken(token: string): Promise<TokenPayload> { try { const payload = jwt.verify(token, this.publicKey, { algorithms: [this.config.algorithm], issuer: this.config.issuer, audience: this.config.audience, }) as TokenPayload; // Check if token has been revoked const isRevoked = await this.tokenBlacklist.isRevoked(payload.jti); if (isRevoked) { throw new TokenRevokedError(); } return payload; } catch (error) { if (error instanceof jwt.TokenExpiredError) { throw new TokenExpiredError(); } throw new InvalidTokenError(); } } async refreshAccessToken(refreshToken: string): Promise<TokenPair> { const tokenHash = this.hashToken(refreshToken); const storedToken = await this.refreshTokenRepository.findByHash(tokenHash); if (!storedToken || storedToken.expiresAt < new Date()) { throw new InvalidRefreshTokenError(); } // Rotate refresh token (one-time use) await this.refreshTokenRepository.delete(storedToken.id); const user = await this.userRepository.findById(storedToken.userId); return { accessToken: this.generateAccessToken(user, storedToken.sessionId), refreshToken: this.generateRefreshToken(user.id, storedToken.sessionId), }; } }
Session Management
// Secure session management with device tracking class SessionManager { async createSession( user: User, deviceInfo: DeviceInfo, ipAddress: string ): Promise<Session> { const sessionId = crypto.randomUUID(); // Check for suspicious activity await this.checkForSuspiciousLogin(user, deviceInfo, ipAddress); const session = await this.sessionRepository.create({ id: sessionId, userId: user.id, deviceFingerprint: this.generateDeviceFingerprint(deviceInfo), userAgent: deviceInfo.userAgent, ipAddress, location: await this.geolocate(ipAddress), createdAt: new Date(), lastActivityAt: new Date(), expiresAt: addDays(new Date(), 30), }); // Enforce maximum concurrent sessions const activeSessions = await this.sessionRepository.countByUserId(user.id); if (activeSessions > this.maxConcurrentSessions) { await this.terminateOldestSession(user.id); } return session; } async validateSession(sessionId: string, currentIp: string): Promise<boolean> { const session = await this.sessionRepository.findById(sessionId); if (!session || session.expiresAt < new Date()) { return false; } // Detect session hijacking attempts if (this.isSignificantLocationChange(session.ipAddress, currentIp)) { await this.flagSuspiciousActivity(session); return false; } // Update last activity await this.sessionRepository.updateLastActivity(sessionId); return true; } async terminateSession(sessionId: string): Promise<void> { const session = await this.sessionRepository.findById(sessionId); if (session) { // Revoke all tokens for this session await this.tokenService.revokeSessionTokens(sessionId); await this.sessionRepository.delete(sessionId); } } async terminateAllUserSessions(userId: string, exceptSessionId?: string): Promise<void> { const sessions = await this.sessionRepository.findByUserId(userId); for (const session of sessions) { if (session.id !== exceptSessionId) { await this.terminateSession(session.id); } } } }
Multi-Factor Authentication
class MFAService { // TOTP (Time-based One-Time Password) async enableTOTP(userId: string): Promise<TOTPSetup> { const secret = authenticator.generateSecret(); // Store encrypted secret await this.mfaRepository.create({ userId, type: 'totp', secret: this.encrypt(secret), enabled: false, // Requires verification to enable }); const user = await this.userRepository.findById(userId); return { secret, qrCodeUrl: authenticator.keyuri(user.email, 'YourApp', secret), }; } async verifyAndEnableTOTP(userId: string, code: string): Promise<boolean> { const mfaConfig = await this.mfaRepository.findByUserId(userId, 'totp'); if (!mfaConfig) { throw new MFANotConfiguredError(); } const secret = this.decrypt(mfaConfig.secret); const isValid = authenticator.verify({ token: code, secret }); if (isValid) { await this.mfaRepository.enable(mfaConfig.id); // Generate backup codes const backupCodes = await this.generateBackupCodes(userId); return true; } return false; } async verifyTOTP(userId: string, code: string): Promise<boolean> { const mfaConfig = await this.mfaRepository.findByUserId(userId, 'totp'); if (!mfaConfig?.enabled) { throw new MFANotEnabledError(); } // Check if it's a backup code if (code.length === 10) { return this.verifyBackupCode(userId, code); } const secret = this.decrypt(mfaConfig.secret); // Allow 1 step tolerance for clock drift return authenticator.verify({ token: code, secret, window: 1, }); } private async generateBackupCodes(userId: string): Promise<string[]> { const codes = Array.from({ length: 10 }, () => crypto.randomBytes(5).toString('hex') ); // Store hashed backup codes await this.backupCodeRepository.replaceAll( userId, codes.map(code => ({ codeHash: this.hashCode(code), used: false, })) ); return codes; } }
Authorization: Controlling Access
Role-Based Access Control (RBAC)
// Define permissions enum Permission { // Patient permissions PATIENT_READ = 'patient:read', PATIENT_WRITE = 'patient:write', PATIENT_DELETE = 'patient:delete', // Medical records MEDICAL_RECORD_READ = 'medical_record:read', MEDICAL_RECORD_WRITE = 'medical_record:write', // Admin USER_MANAGE = 'user:manage', ROLE_MANAGE = 'role:manage', AUDIT_LOG_READ = 'audit_log:read', } // Define roles with permissions const ROLES = { DOCTOR: { name: 'Doctor', permissions: [ Permission.PATIENT_READ, Permission.PATIENT_WRITE, Permission.MEDICAL_RECORD_READ, Permission.MEDICAL_RECORD_WRITE, ], }, NURSE: { name: 'Nurse', permissions: [ Permission.PATIENT_READ, Permission.MEDICAL_RECORD_READ, ], }, ADMIN: { name: 'Admin', permissions: [ Permission.USER_MANAGE, Permission.ROLE_MANAGE, Permission.AUDIT_LOG_READ, ], }, }; // Authorization middleware class AuthorizationMiddleware { requirePermission(...permissions: Permission[]) { return async (req: Request, res: Response, next: NextFunction) => { const user = req.user; if (!user) { return res.status(401).json({ error: 'Unauthorized' }); } const userPermissions = await this.getUserPermissions(user.id); const hasPermission = permissions.every(p => userPermissions.includes(p) ); if (!hasPermission) { // Log unauthorized access attempt await this.auditLog.log({ userId: user.id, action: 'UNAUTHORIZED_ACCESS_ATTEMPT', resource: req.path, requiredPermissions: permissions, timestamp: new Date(), }); return res.status(403).json({ error: 'Forbidden' }); } next(); }; } } // Usage router.get( '/patients/:id', authMiddleware, authorizationMiddleware.requirePermission(Permission.PATIENT_READ), patientController.getById );
Attribute-Based Access Control (ABAC)
// For fine-grained access control interface AccessPolicy { effect: 'allow' | 'deny'; conditions: PolicyCondition[]; } interface PolicyCondition { attribute: string; operator: 'equals' | 'contains' | 'in' | 'greaterThan' | 'custom'; value: unknown; } class ABACEngine { async evaluate( subject: User, action: string, resource: Resource, context: RequestContext ): Promise<boolean> { const policies = await this.policyRepository.findMatching(action, resource.type); for (const policy of policies) { const matches = await this.evaluatePolicy(policy, { subject, resource, context, }); if (matches) { return policy.effect === 'allow'; } } // Default deny return false; } private async evaluatePolicy( policy: AccessPolicy, context: EvaluationContext ): Promise<boolean> { return policy.conditions.every(condition => this.evaluateCondition(condition, context) ); } private evaluateCondition( condition: PolicyCondition, context: EvaluationContext ): boolean { const actualValue = this.resolveAttribute(condition.attribute, context); switch (condition.operator) { case 'equals': return actualValue === condition.value; case 'contains': return Array.isArray(actualValue) && actualValue.includes(condition.value); case 'in': return Array.isArray(condition.value) && condition.value.includes(actualValue); default: return false; } } } // Example: Doctor can only access patients in their department const policy: AccessPolicy = { effect: 'allow', conditions: [ { attribute: 'subject.role', operator: 'equals', value: 'doctor' }, { attribute: 'resource.departmentId', operator: 'in', value: 'subject.departmentIds' }, ], };
Common Security Pitfalls
1. Insecure Token Storage
// ❌ Never store tokens in localStorage (XSS vulnerable) localStorage.setItem('token', accessToken); // ✅ Use httpOnly cookies for refresh tokens res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days path: '/api/auth/refresh', }); // Access tokens can be stored in memory (JavaScript variable) // They're short-lived and refreshed automatically
2. Missing Rate Limiting
const loginRateLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts message: 'Too many login attempts, please try again later', keyGenerator: (req) => req.body.email || req.ip, handler: async (req, res) => { await this.securityAlerts.notify({ type: 'BRUTE_FORCE_ATTEMPT', email: req.body.email, ip: req.ip, }); res.status(429).json({ error: 'Too many attempts' }); }, });
Conclusion
Secure authentication and authorization require:
- Short-lived access tokens with refresh token rotation
- Secure session management with device tracking
- Multi-factor authentication for sensitive operations
- Fine-grained authorization (RBAC or ABAC based on needs)
- Comprehensive audit logging
- Rate limiting and brute force protection
Security is not a feature—it's a continuous process of identifying and mitigating risks.
Related Articles
Security Engineering18 min read
API Security Hardening: A Practitioner's Guide
Secure your APIs with rate limiting, input validation, and CORS configuration. Production-tested checklist covering authentication, encryption, and error handling.
Security Engineering15 min read
Secure Session Management: Patterns and Pitfalls
Implement secure session management with proper cookie settings, token rotation, and logout flows. Covers session fixation, hijacking prevention, and multi-device handling.
Security Engineering16 min read
Protecting Against Injection Attacks: SQL, NoSQL, and XSS
Prevent SQL injection, NoSQL injection, and XSS attacks with validated code patterns. Covers parameterized queries, input sanitization, and CSP configuration.
Payment Integrations28 min read
Stripe Payment Integration: Production Patterns for React and Node.js
Production Stripe integration with Payment Intents, webhooks, and 3D Secure. Covers subscription billing, error handling, and PCI compliance patterns.
Backend Design19 min read
API Design: Choosing Between REST, GraphQL, and gRPC
Compare REST, GraphQL, and gRPC APIs with performance benchmarks and use cases. Learn which API style fits your project based on real production experience.