Loading learning content...
Every secure system begins with a fundamental question: Who are you? Authentication—the process of verifying identity—forms the first and most critical line of defense in any software system. It is the gateway through which legitimate users enter and malicious actors are repelled.
At the low-level design level, authentication is not merely about checking passwords. It encompasses a sophisticated interplay of design patterns, cryptographic primitives, token management, session handling, and multi-factor mechanisms. The way you structure your authentication classes determines whether your system can withstand sophisticated attacks or collapses under the first intrusion attempt.
This page explores authentication design patterns from first principles, examining the architectural decisions, class structures, and implementation strategies that principal engineers employ to build unbreakable identity verification systems.
By the end of this page, you will understand the fundamental authentication patterns, how to design authentication services with proper separation of concerns, token-based authentication architecture, session management design, and multi-factor authentication integration strategies.
Authentication answers the question of identity verification—confirming that a user or system is who they claim to be. Before diving into design patterns, we must establish a clear understanding of what authentication encompasses and how it differs from related security concepts.
The Authentication Triad:
Authentication relies on three fundamental factors, often referred to as the "something you" triad:
Authentication vs Authorization:
A crucial distinction that many developers conflate:
These are complementary but distinct concerns that should be designed as separate subsystems. Authentication must complete successfully before authorization can begin. However, the patterns, interfaces, and classes for each domain should remain loosely coupled.
The Authentication Lifecycle:
Authentication is not a single event but a lifecycle involving multiple phases:
| Phase | Description | Design Considerations |
|---|---|---|
| Registration | Initial identity establishment | Credential strength validation, duplicate checking, secure storage |
| Primary Authentication | Initial identity verification | Credential validation, brute-force protection, timing attack prevention |
| Session Establishment | Creating authenticated state | Token generation, session binding, secure transmission |
| Session Maintenance | Ongoing authenticated access | Token refresh, sliding expiration, activity tracking |
| Credential Management | Password changes, recovery | Secure reset flows, identity verification during changes |
| Termination | Ending authenticated state | Token invalidation, session cleanup, logout propagation |
Authentication failures are catastrophic. A broken authentication system doesn't just expose data—it hands complete control to attackers. 80% of data breaches involve compromised credentials. Your authentication design is quite literally the difference between security and total compromise.
Authentication systems benefit from well-established design patterns that promote security, extensibility, and maintainability. Let's examine the foundational patterns that underpin robust authentication architectures.
Pattern 1: Strategy Pattern for Authentication Methods
Different authentication methods (password, OAuth, SAML, biometrics) share a common interface but have radically different implementations. The Strategy pattern enables interchangeable authentication mechanisms:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
// The core authentication strategy interfaceinterface AuthenticationStrategy { authenticate(credentials: Credentials): Promise<AuthenticationResult>; supports(credentialType: CredentialType): boolean; getName(): string;} // Base credentials type - extended by specific credential typesinterface Credentials { readonly type: CredentialType; readonly timestamp: Date;} interface PasswordCredentials extends Credentials { readonly type: CredentialType.PASSWORD; readonly username: string; readonly password: string; readonly rememberMe?: boolean;} interface OAuthCredentials extends Credentials { readonly type: CredentialType.OAUTH; readonly provider: OAuthProvider; readonly authorizationCode: string; readonly redirectUri: string;} interface BiometricCredentials extends Credentials { readonly type: CredentialType.BIOMETRIC; readonly biometricType: BiometricType; readonly biometricData: Uint8Array; readonly deviceId: string;} // Authentication result encapsulates success or failureclass AuthenticationResult { private constructor( private readonly _success: boolean, private readonly _principal: Principal | null, private readonly _error: AuthenticationError | null, private readonly _requiresAdditionalFactor: boolean, private readonly _partialAuthToken: string | null ) {} static success(principal: Principal): AuthenticationResult { return new AuthenticationResult(true, principal, null, false, null); } static failure(error: AuthenticationError): AuthenticationResult { return new AuthenticationResult(false, null, error, false, null); } static requiresMfa(partialAuthToken: string): AuthenticationResult { return new AuthenticationResult(false, null, null, true, partialAuthToken); } get isSuccess(): boolean { return this._success; } get principal(): Principal | null { return this._principal; } get error(): AuthenticationError | null { return this._error; } get requiresAdditionalFactor(): boolean { return this._requiresAdditionalFactor; } get partialAuthToken(): string | null { return this._partialAuthToken; }}Pattern 2: Chain of Responsibility for Authentication Pipeline
Authentication rarely involves a single check. Multiple validations must occur in sequence: rate limiting, credential validation, account status checks, MFA verification. The Chain of Responsibility pattern enables composable authentication pipelines:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
// Authentication filter in the chainabstract class AuthenticationFilter { protected next: AuthenticationFilter | null = null; setNext(filter: AuthenticationFilter): AuthenticationFilter { this.next = filter; return filter; } async process(context: AuthenticationContext): Promise<AuthenticationResult> { const result = await this.doFilter(context); // If this filter fails or completes authentication, return if (result.isSuccess || result.error || !this.next) { return result; } // Pass to next filter in chain return this.next.process(context); } protected abstract doFilter(context: AuthenticationContext): Promise<AuthenticationResult>;} // Concrete filter implementationsclass RateLimitFilter extends AuthenticationFilter { constructor(private readonly rateLimiter: RateLimiter) { super(); } protected async doFilter(context: AuthenticationContext): Promise<AuthenticationResult> { const key = `auth:${context.ipAddress}:${context.credentials.username}`; if (await this.rateLimiter.isExceeded(key)) { return AuthenticationResult.failure( new RateLimitExceededError("Too many authentication attempts. Please try again later.") ); } // Allow chain to continue return AuthenticationResult.continueChain(); }} class CredentialValidationFilter extends AuthenticationFilter { constructor( private readonly credentialValidator: CredentialValidator, private readonly passwordHasher: PasswordHasher ) { super(); } protected async doFilter(context: AuthenticationContext): Promise<AuthenticationResult> { const user = await this.credentialValidator.findByUsername(context.credentials.username); if (!user) { // Important: Use constant-time comparison to prevent timing attacks await this.passwordHasher.verifyDummy(); return AuthenticationResult.failure(new InvalidCredentialsError()); } const isValid = await this.passwordHasher.verify( context.credentials.password, user.passwordHash ); if (!isValid) { return AuthenticationResult.failure(new InvalidCredentialsError()); } // Attach user to context for subsequent filters context.authenticatedUser = user; return AuthenticationResult.continueChain(); }} class AccountStatusFilter extends AuthenticationFilter { protected async doFilter(context: AuthenticationContext): Promise<AuthenticationResult> { const user = context.authenticatedUser!; if (user.isLocked) { return AuthenticationResult.failure( new AccountLockedError(`Account locked until ${user.lockExpiresAt}`) ); } if (!user.isEmailVerified && this.requiresEmailVerification) { return AuthenticationResult.failure( new EmailNotVerifiedError("Please verify your email before logging in.") ); } if (user.isDisabled) { return AuthenticationResult.failure( new AccountDisabledError("This account has been disabled.") ); } return AuthenticationResult.continueChain(); }} class MfaCheckFilter extends AuthenticationFilter { constructor(private readonly mfaService: MfaService) { super(); } protected async doFilter(context: AuthenticationContext): Promise<AuthenticationResult> { const user = context.authenticatedUser!; if (!user.mfaEnabled) { // MFA not required, continue chain return AuthenticationResult.continueChain(); } // MFA required but not yet provided if (!context.mfaToken) { const partialToken = await this.mfaService.createPartialAuthToken(user); return AuthenticationResult.requiresMfa(partialToken); } // Validate MFA token const mfaValid = await this.mfaService.validateToken(user.id, context.mfaToken); if (!mfaValid) { return AuthenticationResult.failure(new InvalidMfaTokenError()); } return AuthenticationResult.continueChain(); }}The Chain of Responsibility pattern enables you to add, remove, or reorder authentication steps without modifying existing code. Need to add CAPTCHA verification? Insert a new filter. Need geolocation checks? Add another link. This composability is essential for evolving security requirements.
Modern applications predominantly use token-based authentication, particularly JSON Web Tokens (JWT). Understanding the design considerations for token-based systems is essential for building secure, scalable authentication.
Why Tokens Over Sessions?
Traditional session-based authentication stores session state on the server. Token-based authentication shifts state to the client, offering significant architectural advantages:
JWT Structure and Design:
A JWT consists of three parts: Header, Payload, and Signature. Each requires careful design consideration:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
// Token configuration - externalized for flexibilityinterface TokenConfig { readonly accessTokenTtlSeconds: number; readonly refreshTokenTtlSeconds: number; readonly issuer: string; readonly audience: string[]; readonly algorithm: 'RS256' | 'ES256'; // Asymmetric only for security} // Claims that will be embedded in the tokeninterface TokenClaims { readonly sub: string; // Subject (user ID) readonly email: string; readonly roles: string[]; readonly permissions: string[]; readonly sessionId: string; // Links to server-side session for revocation readonly iat: number; // Issued at readonly exp: number; // Expiration readonly iss: string; // Issuer readonly aud: string[]; // Audience} // Token pair returned after successful authenticationinterface TokenPair { readonly accessToken: string; readonly refreshToken: string; readonly accessTokenExpiresAt: Date; readonly refreshTokenExpiresAt: Date; readonly tokenType: 'Bearer';} class JwtTokenService implements TokenService { constructor( private readonly config: TokenConfig, private readonly keyProvider: AsymmetricKeyProvider, private readonly sessionRepository: SessionRepository, private readonly clock: Clock ) {} async generateTokenPair(principal: Principal): Promise<TokenPair> { // Create server-side session for revocation capability const session = await this.sessionRepository.create({ userId: principal.id, createdAt: this.clock.now(), expiresAt: this.clock.nowPlusSeconds(this.config.refreshTokenTtlSeconds), userAgent: principal.userAgent, ipAddress: principal.ipAddress, }); const now = this.clock.nowUnix(); // Access token - short-lived, contains permissions const accessTokenClaims: TokenClaims = { sub: principal.id, email: principal.email, roles: principal.roles, permissions: principal.permissions, sessionId: session.id, iat: now, exp: now + this.config.accessTokenTtlSeconds, iss: this.config.issuer, aud: this.config.audience, }; // Refresh token - long-lived, minimal claims const refreshTokenClaims = { sub: principal.id, sessionId: session.id, type: 'refresh', iat: now, exp: now + this.config.refreshTokenTtlSeconds, iss: this.config.issuer, }; const privateKey = await this.keyProvider.getPrivateKey(); const accessToken = await this.signToken(accessTokenClaims, privateKey); const refreshToken = await this.signToken(refreshTokenClaims, privateKey); return { accessToken, refreshToken, accessTokenExpiresAt: new Date((now + this.config.accessTokenTtlSeconds) * 1000), refreshTokenExpiresAt: new Date((now + this.config.refreshTokenTtlSeconds) * 1000), tokenType: 'Bearer', }; } async validateAccessToken(token: string): Promise<TokenValidationResult> { try { const publicKey = await this.keyProvider.getPublicKey(); const claims = await this.verifyToken(token, publicKey); // Check if session has been revoked const session = await this.sessionRepository.findById(claims.sessionId); if (!session || session.revokedAt) { return TokenValidationResult.invalid(TokenInvalidReason.SESSION_REVOKED); } return TokenValidationResult.valid(claims); } catch (error) { if (error instanceof TokenExpiredError) { return TokenValidationResult.expired(); } return TokenValidationResult.invalid(TokenInvalidReason.MALFORMED); } } async refreshTokens(refreshToken: string): Promise<TokenPair> { const publicKey = await this.keyProvider.getPublicKey(); const claims = await this.verifyToken(refreshToken, publicKey); if (claims.type !== 'refresh') { throw new InvalidTokenTypeError("Expected refresh token"); } const session = await this.sessionRepository.findById(claims.sessionId); if (!session || session.revokedAt) { throw new SessionRevokedError(); } // Rotate refresh token for security await this.sessionRepository.updateLastRefreshed(session.id, this.clock.now()); const principal = await this.loadPrincipal(claims.sub); return this.generateTokenPair(principal); } async revokeSession(sessionId: string): Promise<void> { await this.sessionRepository.revoke(sessionId, this.clock.now()); } async revokeAllUserSessions(userId: string, exceptSessionId?: string): Promise<number> { return this.sessionRepository.revokeAllForUser(userId, this.clock.now(), exceptSessionId); }}Never use symmetric algorithms (HS256) for JWTs in production systems. If an attacker obtains the secret key, they can forge tokens for any user. Use asymmetric algorithms (RS256, ES256) where only the server has the private key. The public key can be safely distributed for validation.
While token-based authentication is stateless in theory, practical systems require server-side session state for features like revocation, concurrent session limits, and activity tracking. The session management subsystem bridges stateless tokens with stateful requirements.
Session Entity Design:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
// Rich session entity with security metadatainterface Session { readonly id: string; readonly userId: string; readonly createdAt: Date; readonly expiresAt: Date; readonly lastActiveAt: Date; readonly ipAddress: string; readonly userAgent: string; readonly deviceFingerprint?: string; readonly location?: GeoLocation; readonly revokedAt?: Date; readonly revocationReason?: SessionRevocationReason;} enum SessionRevocationReason { USER_LOGOUT = 'USER_LOGOUT', PASSWORD_CHANGE = 'PASSWORD_CHANGE', SECURITY_CONCERN = 'SECURITY_CONCERN', ADMIN_ACTION = 'ADMIN_ACTION', CONCURRENT_LIMIT = 'CONCURRENT_LIMIT', EXPIRED = 'EXPIRED',} interface SessionRepository { create(session: CreateSessionDto): Promise<Session>; findById(id: string): Promise<Session | null>; findActiveByUserId(userId: string): Promise<Session[]>; updateLastActive(id: string, timestamp: Date, ipAddress?: string): Promise<void>; revoke(id: string, timestamp: Date, reason: SessionRevocationReason): Promise<void>; revokeAllForUser(userId: string, timestamp: Date, exceptId?: string): Promise<number>; deleteExpired(): Promise<number>;} class SessionManager { constructor( private readonly sessionRepository: SessionRepository, private readonly config: SessionConfig, private readonly eventPublisher: EventPublisher, private readonly clock: Clock ) {} async createSession( userId: string, context: AuthenticationContext ): Promise<Session> { // Enforce concurrent session limit const activeSessions = await this.sessionRepository.findActiveByUserId(userId); if (activeSessions.length >= this.config.maxConcurrentSessions) { // Revoke oldest session const oldest = activeSessions.reduce((a, b) => a.createdAt < b.createdAt ? a : b ); await this.revokeSession(oldest.id, SessionRevocationReason.CONCURRENT_LIMIT); } const session = await this.sessionRepository.create({ userId, createdAt: this.clock.now(), expiresAt: this.clock.nowPlusSeconds(this.config.sessionTtlSeconds), lastActiveAt: this.clock.now(), ipAddress: context.ipAddress, userAgent: context.userAgent, deviceFingerprint: context.deviceFingerprint, location: context.geoLocation, }); await this.eventPublisher.publish(new SessionCreatedEvent(session)); return session; } async validateSession(sessionId: string): Promise<SessionValidationResult> { const session = await this.sessionRepository.findById(sessionId); if (!session) { return SessionValidationResult.invalid(SessionInvalidReason.NOT_FOUND); } if (session.revokedAt) { return SessionValidationResult.invalid(SessionInvalidReason.REVOKED); } if (session.expiresAt < this.clock.now()) { return SessionValidationResult.invalid(SessionInvalidReason.EXPIRED); } // Update last active time for sliding expiration await this.sessionRepository.updateLastActive(sessionId, this.clock.now()); return SessionValidationResult.valid(session); } async revokeSession( sessionId: string, reason: SessionRevocationReason ): Promise<void> { await this.sessionRepository.revoke(sessionId, this.clock.now(), reason); await this.eventPublisher.publish(new SessionRevokedEvent(sessionId, reason)); } async revokeAllUserSessions( userId: string, reason: SessionRevocationReason, currentSessionId?: string ): Promise<number> { const count = await this.sessionRepository.revokeAllForUser( userId, this.clock.now(), currentSessionId // Optionally keep current session active ); await this.eventPublisher.publish( new AllUserSessionsRevokedEvent(userId, count, reason) ); return count; } async getActiveSessions(userId: string): Promise<SessionInfo[]> { const sessions = await this.sessionRepository.findActiveByUserId(userId); return sessions.map(session => ({ id: session.id, createdAt: session.createdAt, lastActiveAt: session.lastActiveAt, location: session.location, deviceInfo: this.parseUserAgent(session.userAgent), isCurrent: false, // Set by caller based on current session ID })); }}Combining stateless tokens with server-side sessions provides the best of both worlds: horizontal scalability from stateless validation, plus revocation capability and session management from server-side state. The sessionId embedded in the token links the two worlds.
Multi-Factor Authentication (MFA) significantly increases security by requiring multiple verification factors. Designing MFA properly involves handling partial authentication states, multiple factor types, and recovery scenarios.
MFA Factor Types:
| Factor Type | Security Level | User Experience | Implementation Complexity |
|---|---|---|---|
| TOTP (Authenticator App) | High | Moderate | Low |
| SMS OTP | Medium (SIM swap risk) | Good | Medium |
| Email OTP | Medium | Good | Low |
| Hardware Key (FIDO2/WebAuthn) | Very High | Good (once setup) | High |
| Push Notification | High | Excellent | High |
| Backup Codes | Recovery mechanism | Moderate | Low |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
// MFA factor interface - Strategy pattern for different factor typesinterface MfaFactor { readonly type: MfaFactorType; validate(userId: string, input: MfaInput): Promise<MfaValidationResult>; generateChallenge(userId: string): Promise<MfaChallenge>; isAvailable(userId: string): Promise<boolean>;} // TOTP implementationclass TotpFactor implements MfaFactor { readonly type = MfaFactorType.TOTP; constructor( private readonly userMfaRepository: UserMfaRepository, private readonly totpGenerator: TotpGenerator, private readonly clock: Clock ) {} async validate(userId: string, input: TotpInput): Promise<MfaValidationResult> { const mfaConfig = await this.userMfaRepository.findByUserIdAndType( userId, MfaFactorType.TOTP ); if (!mfaConfig || !mfaConfig.secret) { return MfaValidationResult.factorNotConfigured(); } // Validate with time window for clock drift const isValid = this.totpGenerator.validate( input.code, mfaConfig.secret, { window: 1, // Allow ±30 seconds time: this.clock.nowUnix(), } ); if (!isValid) { await this.recordFailedAttempt(userId); return MfaValidationResult.invalid(); } // Prevent code reuse if (await this.isCodeRecentlyUsed(userId, input.code)) { return MfaValidationResult.codeAlreadyUsed(); } await this.recordUsedCode(userId, input.code); return MfaValidationResult.valid(); } async generateChallenge(userId: string): Promise<TotpChallenge> { // TOTP doesn't require server-side challenge generation return { type: MfaFactorType.TOTP, message: "Enter the 6-digit code from your authenticator app", }; }} // WebAuthn/FIDO2 implementation for hardware keysclass WebAuthnFactor implements MfaFactor { readonly type = MfaFactorType.WEBAUTHN; constructor( private readonly webAuthnRepository: WebAuthnCredentialRepository, private readonly challengeStore: ChallengeStore, private readonly relyingParty: RelyingParty ) {} async generateChallenge(userId: string): Promise<WebAuthnChallenge> { const credentials = await this.webAuthnRepository.findByUserId(userId); const challenge = crypto.getRandomValues(new Uint8Array(32)); await this.challengeStore.store(userId, challenge, 300); // 5 minute TTL return { type: MfaFactorType.WEBAUTHN, challenge: base64url.encode(challenge), rpId: this.relyingParty.id, allowCredentials: credentials.map(c => ({ type: 'public-key', id: c.credentialId, })), timeout: 60000, userVerification: 'preferred', }; } async validate(userId: string, input: WebAuthnInput): Promise<MfaValidationResult> { const expectedChallenge = await this.challengeStore.retrieve(userId); if (!expectedChallenge) { return MfaValidationResult.challengeExpired(); } const credential = await this.webAuthnRepository.findByCredentialId( input.credentialId ); if (!credential || credential.userId !== userId) { return MfaValidationResult.invalid(); } // Verify the assertion const isValid = await this.verifyAssertion( input, expectedChallenge, credential.publicKey ); if (!isValid) { return MfaValidationResult.invalid(); } // Update signature counter for clone detection await this.webAuthnRepository.updateSignatureCounter( credential.id, input.signatureCounter ); return MfaValidationResult.valid(); }} // MFA orchestration serviceclass MfaService { constructor( private readonly factors: Map<MfaFactorType, MfaFactor>, private readonly userMfaRepository: UserMfaRepository, private readonly partialAuthStore: PartialAuthStore, private readonly eventPublisher: EventPublisher ) {} async createPartialAuthToken(user: User): Promise<string> { const token = crypto.randomUUID(); await this.partialAuthStore.store(token, { userId: user.id, completedFactor: 'password', requiredFactors: await this.getRequiredFactors(user.id), createdAt: new Date(), expiresAt: new Date(Date.now() + 300000), // 5 minutes }); return token; } async validateFactor( partialToken: string, factorType: MfaFactorType, input: MfaInput ): Promise<MfaAuthResult> { const partialAuth = await this.partialAuthStore.retrieve(partialToken); if (!partialAuth || partialAuth.expiresAt < new Date()) { return MfaAuthResult.sessionExpired(); } const factor = this.factors.get(factorType); if (!factor) { return MfaAuthResult.unsupportedFactor(); } const result = await factor.validate(partialAuth.userId, input); if (!result.isValid) { return MfaAuthResult.factorFailed(result.reason); } // Check if all required factors are now complete const remainingFactors = partialAuth.requiredFactors.filter( f => f !== factorType && !partialAuth.completedFactors?.includes(f) ); if (remainingFactors.length === 0) { await this.partialAuthStore.delete(partialToken); return MfaAuthResult.complete(partialAuth.userId); } // More factors required await this.partialAuthStore.update(partialToken, { completedFactors: [...(partialAuth.completedFactors || []), factorType], }); return MfaAuthResult.requiresAdditionalFactor(remainingFactors); }}Always provide backup/recovery codes when enabling MFA. Store them hashed (like passwords). Limit to 10-15 codes. Each code should be single-use. Show remaining count to users. Alert users when running low.
Bringing all the patterns together, we design a cohesive authentication service that orchestrates the entire authentication flow. This service composes the various components while maintaining clean separation of concerns.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
// The main authentication service - facade over authentication subsystemsclass AuthenticationService { private readonly authenticationChain: AuthenticationFilter; constructor( private readonly strategyRegistry: AuthenticationStrategyRegistry, private readonly tokenService: TokenService, private readonly sessionManager: SessionManager, private readonly mfaService: MfaService, private readonly auditLogger: SecurityAuditLogger, private readonly eventPublisher: EventPublisher, rateLimiter: RateLimiter, credentialValidator: CredentialValidator, passwordHasher: PasswordHasher ) { // Build the authentication filter chain this.authenticationChain = this.buildAuthenticationChain( rateLimiter, credentialValidator, passwordHasher ); } private buildAuthenticationChain( rateLimiter: RateLimiter, credentialValidator: CredentialValidator, passwordHasher: PasswordHasher ): AuthenticationFilter { const rateLimit = new RateLimitFilter(rateLimiter); const credentialValidation = new CredentialValidationFilter( credentialValidator, passwordHasher ); const accountStatus = new AccountStatusFilter(); const mfaCheck = new MfaCheckFilter(this.mfaService); // Chain filters together rateLimit .setNext(credentialValidation) .setNext(accountStatus) .setNext(mfaCheck); return rateLimit; } async authenticate(request: AuthenticationRequest): Promise<AuthenticationResponse> { const context = this.createContext(request); try { // Run through authentication chain const result = await this.authenticationChain.process(context); if (result.requiresAdditionalFactor) { await this.auditLogger.logMfaRequired(context); return AuthenticationResponse.mfaRequired( result.partialAuthToken, await this.mfaService.getAvailableFactors(context.authenticatedUser!.id) ); } if (!result.isSuccess) { await this.recordFailedAttempt(context, result.error!); return AuthenticationResponse.failure(result.error!); } // Authentication successful - issue tokens return await this.completeAuthentication(context); } catch (error) { await this.auditLogger.logAuthenticationError(context, error); throw error; } } async completeMfaAuthentication( partialToken: string, factorType: MfaFactorType, input: MfaInput, context: RequestContext ): Promise<AuthenticationResponse> { const result = await this.mfaService.validateFactor( partialToken, factorType, input ); if (!result.isComplete) { if (result.isExpired) { return AuthenticationResponse.failure( new MfaSessionExpiredError() ); } if (result.requiresAdditionalFactor) { return AuthenticationResponse.mfaRequired( partialToken, result.remainingFactors ); } return AuthenticationResponse.failure(new InvalidMfaTokenError()); } // MFA complete - load user and issue tokens const user = await this.loadUser(result.userId); const authContext = this.createContextForUser(user, context); return await this.completeAuthentication(authContext); } private async completeAuthentication( context: AuthenticationContext ): Promise<AuthenticationResponse> { const user = context.authenticatedUser!; // Create session const session = await this.sessionManager.createSession(user.id, context); // Generate tokens const principal = this.buildPrincipal(user, session.id); const tokens = await this.tokenService.generateTokenPair(principal); // Log successful authentication await this.auditLogger.logSuccessfulAuthentication(context, session); // Publish event for other systems await this.eventPublisher.publish( new UserAuthenticatedEvent(user.id, session.id, context.ipAddress) ); // Check for suspicious patterns await this.checkForAnomalies(context, session); return AuthenticationResponse.success(tokens, this.sanitizeUser(user)); } async logout(sessionId: string, userId: string): Promise<void> { await this.sessionManager.revokeSession( sessionId, SessionRevocationReason.USER_LOGOUT ); await this.eventPublisher.publish(new UserLoggedOutEvent(userId, sessionId)); await this.auditLogger.logLogout(userId, sessionId); } async logoutAllDevices(userId: string, currentSessionId?: string): Promise<void> { const revokedCount = await this.sessionManager.revokeAllUserSessions( userId, SessionRevocationReason.USER_LOGOUT, currentSessionId ); await this.auditLogger.logLogoutAllDevices(userId, revokedCount); } private async checkForAnomalies( context: AuthenticationContext, session: Session ): Promise<void> { // Check for impossible travel const recentSessions = await this.sessionManager.getRecentSessions( context.authenticatedUser!.id, 10 ); if (this.detectImpossibleTravel(session, recentSessions)) { await this.eventPublisher.publish( new SuspiciousLoginEvent(session, 'IMPOSSIBLE_TRAVEL') ); } // Check for new device if (!recentSessions.some(s => s.deviceFingerprint === session.deviceFingerprint)) { await this.eventPublisher.publish( new NewDeviceLoginEvent(session) ); } }}Notice how the AuthenticationService acts as a facade, delegating to specialized services: TokenService for JWT operations, SessionManager for session lifecycle, MfaService for multi-factor flows, and AuditLogger for security logging. Each component has a single, well-defined responsibility.
Authentication systems are prime targets for attackers. Every design decision must consider security implications:
OWASP identifies Broken Authentication as a top vulnerability. Common issues include: weak password requirements, credential stuffing allowing, user enumeration through error messages, session tokens in URLs, and missing MFA for sensitive operations.
Authentication design patterns form the bedrock of secure systems. Let's consolidate the key architectural insights:
What's next:
With authentication patterns established, the next page explores authorization design patterns—controlling what authenticated users can do within the system.
You now understand the fundamental authentication design patterns used in production systems. You can design authentication services with proper separation of concerns, implement token-based authentication with session management, and integrate multi-factor authentication securely.