Assist_Design/docs/AUTHENTICATION-SECURITY.md

722 lines
18 KiB
Markdown

# 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<TokenPair> {
// 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<TokenPair> {
// 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<void> {
await this.redis.del(`refresh:${token}`);
}
private async checkFamilyCompromised(family: string): Promise<boolean> {
return this.redis.exists(`family:compromised:${family}`) === 1;
}
private async revokeTokenFamily(family: string): Promise<void> {
// 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<void> {
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<boolean> {
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<boolean> {
// Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('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<Invoice[]> {
// Automatically filtered by userId
return this.whmcsInvoiceService.getInvoices(parseInt(userId));
}
async getInvoice(userId: string, invoiceId: number): Promise<Invoice> {
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<void> {
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<void> {
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<void> {
await this.redis.del(`rate:login:${identifier}`);
}
async checkCaptchaRequired(identifier: string): Promise<boolean> {
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<boolean> {
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<string> {
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<boolean> {
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