# Authentication & Security Design **Customer Portal - Security Architecture & Implementation** --- ## Table of Contents 1. [Security Overview](#security-overview) 2. [Authentication Architecture](#authentication-architecture) 3. [Token Management](#token-management) 4. [Authorization & Access Control](#authorization--access-control) 5. [Rate Limiting](#rate-limiting) 6. [Security Best Practices](#security-best-practices) 7. [Data Protection](#data-protection) --- ## Security Overview The Customer Portal implements defense-in-depth security with multiple layers of protection, from secure authentication to PII redaction in logs. ### Security Principles 1. **Never expose sensitive data** [[memory:6689308]] 2. **Production-ready error handling** - No stack traces to users 3. **Principle of least privilege** - Minimal access rights 4. **Defense in depth** - Multiple security layers 5. **Audit everything** - Comprehensive logging 6. **Fail securely** - Secure defaults --- ## Authentication Architecture ### Authentication Flow ``` ┌──────────────┐ │ Customer │ └──────┬───────┘ │ │ 1. Submit credentials │ ┌──────▼────────────────────────────┐ │ Portal │ │ POST /auth/login │ └──────┬────────────────────────────┘ │ │ 2. Forward to BFF │ ┌──────▼────────────────────────────┐ │ BFF Auth Module │ │ • Validate credentials │ │ • Check rate limits │ │ • Verify WHMCS link (optional) │ └──────┬────────────────────────────┘ │ │ 3. Issue tokens │ ┌──────▼────────────────────────────┐ │ Token Service │ │ • Access Token (JWT, 15 min) │ │ • Refresh Token (Redis, 7 days) │ └──────┬────────────────────────────┘ │ │ 4. Return tokens │ ┌──────▼────────────────────────────┐ │ Portal │ │ • Store Access Token (memory) │ │ • Store Refresh Token (cookie) │ └───────────────────────────────────┘ ``` ### Auth Module Structure ``` modules/auth/ ├── application/ │ └── auth.facade.ts # Orchestrates auth workflows ├── presentation/ │ ├── http/ │ │ ├── auth.controller.ts # HTTP endpoints │ │ ├── guards/ │ │ │ ├── global-auth.guard.ts │ │ │ ├── auth-throttle.guard.ts │ │ │ └── failed-login-throttle.guard.ts │ │ └── interceptors/ │ │ └── login-result.interceptor.ts │ └── strategies/ │ ├── local.strategy.ts # Password authentication │ └── jwt.strategy.ts # JWT validation ├── infra/ │ ├── token/ │ │ ├── token.service.ts # Token issuance/rotation │ │ └── token-blacklist.service.ts │ ├── rate-limiting/ │ │ └── auth-rate-limit.service.ts │ └── workflows/ │ ├── signup-workflow.service.ts │ ├── password-workflow.service.ts │ └── whmcs-link-workflow.service.ts └── decorators/ └── public.decorator.ts # @Public() decorator ``` --- ## Token Management ### Token Types **Access Token (JWT)**: - **Purpose**: Authorize API requests - **Lifetime**: 15 minutes - **Storage**: Memory (frontend) - **Format**: JWT with user claims ```typescript interface AccessTokenPayload { sub: string; // User ID email: string; username: string; iat: number; // Issued at exp: number; // Expires at } ``` **Refresh Token**: - **Purpose**: Obtain new access tokens - **Lifetime**: 7 days - **Storage**: Redis (backend), httpOnly cookie (frontend) - **Format**: Random string (UUIDv4) - **Family tracking**: Token rotation for security ### Token Service Implementation ```typescript @Injectable() export class TokenService { constructor( private readonly jwtService: JwtService, private readonly redis: Redis, private readonly config: ConfigService ) {} async issueTokens(userId: string): Promise { // Generate access token (JWT) const accessToken = this.jwtService.sign({ sub: userId, email: user.email, username: user.username }, { expiresIn: '15m' }); // Generate refresh token (random) const refreshToken = randomUUID(); const tokenFamily = randomUUID(); // Store refresh token in Redis await this.redis.setex( `refresh:${refreshToken}`, 7 * 24 * 60 * 60, // 7 days JSON.stringify({ userId, family: tokenFamily, createdAt: new Date().toISOString() }) ); return { accessToken, refreshToken }; } async rotateRefreshToken(oldToken: string): Promise { // Get old token data const data = await this.redis.get(`refresh:${oldToken}`); if (!data) { throw new UnauthorizedException('Invalid refresh token'); } const { userId, family } = JSON.parse(data); // Delete old token await this.redis.del(`refresh:${oldToken}`); // Check if family is compromised (reuse detected) const familyCompromised = await this.checkFamilyCompromised(family); if (familyCompromised) { await this.revokeTokenFamily(family); throw new UnauthorizedException('Token reuse detected'); } // Issue new tokens with same family return this.issueTokens(userId); } async revokeToken(token: string): Promise { await this.redis.del(`refresh:${token}`); } private async checkFamilyCompromised(family: string): Promise { return this.redis.exists(`family:compromised:${family}`) === 1; } private async revokeTokenFamily(family: string): Promise { // Mark family as compromised await this.redis.setex(`family:compromised:${family}`, 60 * 60, '1'); // Delete all tokens in family const keys = await this.redis.keys(`refresh:*`); for (const key of keys) { const data = await this.redis.get(key); if (data && JSON.parse(data).family === family) { await this.redis.del(key); } } } } ``` ### Token Blacklist Revoked access tokens are blacklisted until they naturally expire: ```typescript @Injectable() export class TokenBlacklistService { constructor(private readonly redis: Redis) {} async blacklist(token: string, expiresAt: Date): Promise { const ttl = Math.floor((expiresAt.getTime() - Date.now()) / 1000); if (ttl > 0) { await this.redis.setex(`blacklist:${token}`, ttl, '1'); } } async isBlacklisted(token: string): Promise { return (await this.redis.exists(`blacklist:${token}`)) === 1; } } ``` --- ## Authorization & Access Control ### Global Auth Guard All endpoints require authentication by default: ```typescript @Injectable() export class GlobalAuthGuard extends AuthGuard('jwt') { constructor( private readonly reflector: Reflector, private readonly blacklist: TokenBlacklistService ) { super(); } async canActivate(context: ExecutionContext): Promise { // Check for @Public() decorator const isPublic = this.reflector.getAllAndOverride('isPublic', [ context.getHandler(), context.getClass() ]); if (isPublic) { return true; } // Validate JWT const isValid = await super.canActivate(context); if (!isValid) { return false; } // Check blacklist const request = context.switchToHttp().getRequest(); const token = this.extractToken(request); if (await this.blacklist.isBlacklisted(token)) { throw new UnauthorizedException('Token has been revoked'); } return true; } private extractToken(request: any): string { const [type, token] = request.headers.authorization?.split(' ') ?? []; return type === 'Bearer' ? token : ''; } } ``` ### Public Endpoints Use `@Public()` decorator to exempt endpoints from authentication: ```typescript @Controller('auth') export class AuthController { @Post('login') @Public() async login(@Body() dto: LoginDto) { return this.authFacade.login(dto.email, dto.password); } @Post('refresh') @Public() async refresh(@Req() req: Request) { const refreshToken = req.cookies['refresh_token']; return this.authFacade.refreshTokens(refreshToken); } @Post('logout') async logout(@Req() req: Request) { return this.authFacade.logout(req.user.sub, req.cookies['refresh_token']); } } ``` ### Row-Level Security Users can only access their own data: ```typescript @Injectable() export class InvoiceService { async getInvoices(userId: string): Promise { // Automatically filtered by userId return this.whmcsInvoiceService.getInvoices(parseInt(userId)); } async getInvoice(userId: string, invoiceId: number): Promise { const invoice = await this.whmcsInvoiceService.getInvoice(invoiceId); // Verify ownership if (invoice.userId !== parseInt(userId)) { throw new ForbiddenException('Access denied'); } return invoice; } } ``` --- ## Rate Limiting ### Rate Limit Configuration ```typescript // Login attempts LOGIN_RATE_LIMIT_LIMIT=5 // 5 attempts LOGIN_RATE_LIMIT_TTL=900000 // 15 minutes // Signup attempts SIGNUP_RATE_LIMIT_LIMIT=3 // 3 attempts SIGNUP_RATE_LIMIT_TTL=3600000 // 1 hour // Token refresh AUTH_REFRESH_RATE_LIMIT_LIMIT=10 // 10 attempts AUTH_REFRESH_RATE_LIMIT_TTL=300000 // 5 minutes // Password reset PASSWORD_RESET_RATE_LIMIT_LIMIT=3 // 3 attempts PASSWORD_RESET_RATE_LIMIT_TTL=3600000 // 1 hour ``` ### Rate Limit Service ```typescript @Injectable() export class AuthRateLimitService { constructor(private readonly redis: Redis) {} async checkLoginAttempts(identifier: string): Promise { const key = `rate:login:${identifier}`; const attempts = await this.redis.get(key); if (attempts && parseInt(attempts) >= 5) { const ttl = await this.redis.ttl(key); throw new TooManyRequestsException( `Too many login attempts. Try again in ${Math.ceil(ttl / 60)} minutes.` ); } } async incrementLoginAttempts(identifier: string): Promise { const key = `rate:login:${identifier}`; const current = await this.redis.incr(key); if (current === 1) { await this.redis.expire(key, 900); // 15 minutes } } async clearLoginAttempts(identifier: string): Promise { await this.redis.del(`rate:login:${identifier}`); } async checkCaptchaRequired(identifier: string): Promise { const key = `rate:login:${identifier}`; const attempts = await this.redis.get(key); return attempts && parseInt(attempts) >= 3; // Show CAPTCHA after 3 attempts } } ``` ### Failed Login Throttle Guard ```typescript @Injectable() export class FailedLoginThrottleGuard implements CanActivate { constructor(private readonly rateLimitService: AuthRateLimitService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const identifier = request.body.email || request.ip; await this.rateLimitService.checkLoginAttempts(identifier); return true; } } ``` --- ## Security Best Practices ### Password Security **Hashing**: Argon2id (OWASP recommended) ```typescript import * as argon2 from 'argon2'; @Injectable() export class PasswordService { async hashPassword(password: string): Promise { return argon2.hash(password, { type: argon2.argon2id, memoryCost: 65536, // 64 MB timeCost: 3, // 3 iterations parallelism: 4 // 4 threads }); } async verifyPassword(hash: string, password: string): Promise { try { return await argon2.verify(hash, password); } catch (error) { return false; } } } ``` **Password Requirements**: - Minimum 8 characters - At least one uppercase letter - At least one lowercase letter - At least one number - At least one special character ```typescript export const passwordSchema = z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one number') .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'); ``` ### CSRF Protection **Double Submit Cookie Pattern**: ```typescript @Injectable() export class CsrfService { generateToken(): string { return randomBytes(32).toString('hex'); } validateToken(cookieToken: string, headerToken: string): boolean { if (!cookieToken || !headerToken) { return false; } return timingSafeEqual( Buffer.from(cookieToken), Buffer.from(headerToken) ); } } // Middleware @Injectable() export class CsrfMiddleware implements NestMiddleware { constructor(private readonly csrfService: CsrfService) {} use(req: Request, res: Response, next: NextFunction) { // Generate token for new sessions if (!req.cookies['csrf-secret']) { const token = this.csrfService.generateToken(); res.cookie('csrf-secret', token, { httpOnly: true, secure: true, sameSite: 'strict' }); } // Validate token for mutating requests if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) { const cookieToken = req.cookies['csrf-secret']; const headerToken = req.headers['x-csrf-token'] as string; if (!this.csrfService.validateToken(cookieToken, headerToken)) { throw new ForbiddenException('Invalid CSRF token'); } } next(); } } ``` ### Error Handling **Never expose sensitive information** [[memory:6689308]]: ```typescript @Catch() export class GlobalExceptionFilter implements ExceptionFilter { constructor(private readonly logger: Logger) {} catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); // Log full error details this.logger.error('Request failed', { url: request.url, method: request.method, error: exception.message, stack: exception.stack }); // Return user-friendly message const status = exception instanceof HttpException ? exception.getStatus() : 500; const userMessage = exception instanceof HttpException ? exception.message : 'An unexpected error occurred'; response.status(status).json({ success: false, message: userMessage, // NEVER include: stack, internal details, database errors }); } } ``` --- ## Data Protection ### PII Redaction **Automatic redaction in logs**: ```typescript const sensitiveFields = [ 'password', 'ssn', 'creditCard', 'cvv', 'accessToken', 'refreshToken' ]; function redactSensitiveData(data: any): any { if (typeof data !== 'object' || data === null) { return data; } const redacted = { ...data }; for (const key of Object.keys(redacted)) { if (sensitiveFields.includes(key)) { redacted[key] = '[REDACTED]'; } else if (typeof redacted[key] === 'object') { redacted[key] = redactSensitiveData(redacted[key]); } } return redacted; } // Logger setup const logger = pino({ redact: { paths: [ 'password', 'req.headers.authorization', 'req.body.password', 'req.body.creditCard' ], remove: true } }); ``` ### Secure Headers ```typescript app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })); ``` ### Secure Cookies ```typescript response.cookie('refresh_token', refreshToken, { httpOnly: true, // Prevent XSS secure: true, // HTTPS only sameSite: 'strict', // Prevent CSRF maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days path: '/auth/refresh' // Limit scope }); ``` --- ## Audit Trail ### Audit Logging ```typescript @Injectable() export class AuditService { constructor(private readonly logger: Logger) {} logAuthEvent(event: string, userId: string, metadata?: any) { this.logger.info({ event, userId, timestamp: new Date().toISOString(), ...metadata }, 'Auth event'); } logAccessEvent(userId: string, resource: string, action: string) { this.logger.info({ event: 'resource_access', userId, resource, action, timestamp: new Date().toISOString() }, 'Resource access'); } logSecurityEvent(event: string, metadata: any) { this.logger.warn({ event, timestamp: new Date().toISOString(), ...metadata }, 'Security event'); } } // Usage await this.auditService.logAuthEvent('login_success', userId, { ip: request.ip }); await this.auditService.logSecurityEvent('rate_limit_exceeded', { ip: request.ip, endpoint: '/auth/login' }); ``` --- ## Related Documentation - [System Architecture](./SYSTEM-ARCHITECTURE.md) - Overall system design - [Integration & Data Flow](./INTEGRATION-DATAFLOW.md) - External system integration --- **Last Updated**: October 2025 **Status**: Active - Production System