16 KiB
ADR-016: Security & Authentication Strategy
Status: ✅ Accepted Date: 2026-02-24 Decision Makers: Security Team, System Architect Related Documents: ADR-004: RBAC Implementation, ADR-007: API Design Version Applicability: v1.8.0+ Next Review: 2026-08-01 (6-month cycle)
Gap Analysis & Requirement Linking
ปิด Gap จาก Requirements:
| Gap/Requirement | แหล่งที่มา | วิธีการแก้ไขใน ADR นี้ |
|---|---|---|
| Authentication & Authorization | Product Vision - Security Requirements | JWT + RBAC 4-level implementation |
| Data Protection | Acceptance Criteria - AC-SEC-001 | AES-256 encryption at rest + HTTPS in transit |
| Audit Trail | Business Rules | Comprehensive security event logging |
| Session Management | Edge Cases - Session timeout | Stateless JWT + 15min access token expiry |
| Input Validation | API Design - Validation layer | Class-validator + Zod + Sanitization |
แก้ไขความขัดแย้ง:
- Conflict: Cross-domain authentication complexity vs. User Experience
- Resolution: Chose Bearer tokens over HTTP-only cookies for Next.js ↔ NestJS communication
- Trade-off: Slightly reduced XSS protection for improved developer experience
Impact Analysis
Affected Components (ส่วนประกอบที่ได้รับผลกระทบ):
| Component | ผลกระทบ | ความสำคัญ |
|---|---|---|
| Backend Auth Module | JWT implementation + Guards | 🔴 Critical |
| Frontend Auth Store | Zustand token management | 🔴 Critical |
| Database Schema | refresh_tokens table | 🔴 Critical |
| API Controllers | @UseGuards(JwtAuthGuard) | 🟡 Important |
| Middleware | Helmet + CORS configuration | 🟡 Important |
| User Service | Password hashing with bcrypt | 🟡 Important |
| Audit Log Service | Security event tracking | 🟡 Important |
| Frontend Login Page | Token storage logic | 🟢 Guidelines |
| Environment Config | JWT secrets + Encryption keys | 🔴 Critical |
Required Changes (การเปลี่ยนแปลงที่ต้องดำเนินการ):
Backend (NestJS)
- Implement AuthService with JWT
- Create JwtAuthGuard
- Add refresh_tokens entity
- Configure Helmet + CORS
- Add rate limiting (Throttler)
- Implement audit logging
Frontend (Next.js)
- Create auth store (Zustand)
- Update API client with Bearer token
- Add token refresh logic
- Update login/logout flows
Infrastructure
- Environment variables for secrets
- HTTPS/TLS configuration
- Database encryption setup
Context and Problem Statement
LCBP3-DMS จัดการเอกสารสำคัญของโปรเจกต์ ต้องการ Security strategy ที่ครอบคลุม Authentication, Authorization, Data protection, และ Security best practices
ปัญหาที่ต้องแก้:
- Authentication: ใช้วิธีไหนในการยืนยันตัวตน
- Session Management: จัดการ Session อย่างไร
- Password Security: เก็บ Password อย่างไรให้ปลอดภัย
- Data Encryption: Encrypt ข้อมูลอย่างไร
- Security Headers: HTTP Headers ที่ต้องมี
- Input Validation: ป้องกัน Injection attacks
- Rate Limiting: ป้องกัน Brute force attacks
Decision Drivers
- 🔒 Security First: ความปลอดภัยเป็นสำคัญที่สุด
- ✅ Industry Standards: ใช้ Standard practices (OWASP)
- 🎯 User Experience: ไม่ซับซ้อนเกินไป
- 📝 Audit Trail: บันทึก Security events ทั้งหมด
- 🔄 Token Refresh: Session management ที่สะดวก
Decision Outcome
1. Authentication Strategy
Chosen: JWT (JSON Web Tokens) with Bearer Token Strategy (Stored in LocalStorage via Zustand)
Note: Initial plan was HTTP-only cookies, but shifted to Bearer tokens to ease cross-domain Next.js to NestJS communication.
// File: src/auth/auth.service.ts
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly usersService: UsersService
) {}
async login(credentials: LoginDto): Promise<{ tokens }> {
const user = await this.validateUser(credentials);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = {
sub: user.user_id,
username: user.username,
roles: user.roles,
};
// Generate tokens
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m', // Short-lived
});
const refreshToken = this.jwtService.sign(payload, {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d', // Long-lived
});
// Store refresh token (hashed) in database
await this.storeRefreshToken(user.user_id, refreshToken);
return { accessToken, refreshToken };
}
private async validateUser(credentials: LoginDto) {
const user = await this.usersService.findByUsername(credentials.username);
if (!user) return null;
// Use bcrypt for password comparison
const isValid = await bcrypt.compare(credentials.password, user.password_hash);
return isValid ? user : null;
}
}
2. Password Security
Strategy: bcrypt with salt rounds = 10 (Current implementation defaults to 10 via genSalt())
Note: Code currently uses bcrypt.genSalt() without arguments, defaulting to 10 rounds. If 12 is strictly required, codebase needs updating.
import * as bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
// Hash password
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
// Verify password
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Password Policy:
- Minimum 8 characters
- Mix of uppercase, lowercase, numbers
- No common passwords (check against dictionary)
- Password history (last 5 passwords)
- Force change every 90 days (optional)
3. JWT Guard (Authorization)
// File: src/common/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw new UnauthorizedException(info?.message || 'Unauthorized');
}
return user;
}
}
4. Data Encryption
At Rest:
- Database: Use MariaDB encryption at column level (for sensitive fields)
- Files: Encrypt before storing (AES-256)
import * as crypto from 'crypto';
const algorithm = 'aes-256-gcm';
const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
function encrypt(text: string): { encrypted: string; iv: string; tag: string } {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex'),
};
}
function decrypt(encrypted: string, iv: string, tag: string): string {
const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(tag, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
In Transit:
- HTTPS only (TLS 1.3)
- HSTS enabled
- Certificate from trusted CA
5. Security Headers
// File: src/main.ts
import helmet from 'helmet';
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' },
xssFilter: true,
noSniff: true,
})
);
// CORS
app.enableCors({
origin: process.env.FRONTEND_URL,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
6. Input Validation
Strategy: Class-validator + Zod + Custom Sanitization
// DTO Validation
import { IsString, IsEmail, MinLength, Matches } from 'class-validator';
export class LoginDto {
@IsString()
@MinLength(3)
username: string;
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;
}
// SQL Injection Prevention (TypeORM handles this)
// Use parameterized queries ALWAYS
// XSS Prevention
import * as sanitizeHtml from 'sanitize-html';
function sanitizeInput(input: string): string {
return sanitizeHtml(input, {
allowedTags: [], // No HTML tags
allowedAttributes: {},
});
}
7. Rate Limiting
// File: src/common/guards/rate-limit.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
protected getTracker(req: Request): string {
// Track by IP + User ID (if authenticated)
return req.ip + (req.user?.user_id || '');
}
}
// Apply to login endpoint
@Controller('auth')
@UseGuards(CustomThrottlerGuard)
export class AuthController {
@Post('login')
@Throttle(5, 60) // 5 attempts per minute
async login(@Body() credentials: LoginDto) {
return this.authService.login(credentials);
}
}
8. Session Management
Strategy: Stateless JWT + Refresh Token in Database
// Refresh token table
@Entity('refresh_tokens')
export class RefreshToken {
@PrimaryGeneratedColumn()
token_id: number;
@Column()
user_id: number;
@Column()
token_hash: string; // SHA-256 hash of token
@Column()
expires_at: Date;
@Column({ default: false })
is_revoked: boolean;
@CreateDateColumn()
created_at: Date;
}
// Token refresh endpoint
@Post('refresh')
async refresh(@Body('refreshToken') token: string) {
const payload = this.jwtService.verify(token, {
secret: process.env.JWT_REFRESH_SECRET,
});
// Check if token is revoked
const storedToken = await this.findRefreshToken(token);
if (!storedToken || storedToken.is_revoked) {
throw new UnauthorizedException('Invalid refresh token');
}
// Generate new access token
const newAccessToken = this.jwtService.sign({
sub: payload.sub,
username: payload.username,
roles: payload.roles,
});
return { accessToken: newAccessToken };
}
9. Audit Logging (Security Events)
// Log all security-related events
await this.auditLogService.create({
user_id: user.user_id,
action: 'LOGIN_SUCCESS',
entity_type: 'auth',
ip_address: req.ip,
user_agent: req.headers['user-agent'],
});
// Track failed login attempts
await this.auditLogService.create({
action: 'LOGIN_FAILED',
entity_type: 'auth',
ip_address: req.ip,
details: { username: credentials.username },
});
Security Checklist
Application Security
- JWT authentication with short-lived tokens (Bearer Token)
- Password hashing with bcrypt (12 rounds)
- HTTPS only (TLS 1.3)
- Security headers (Helmet.js)
- CORS properly configured
- Input validation (class-validator)
- SQL injection prevention (TypeORM)
- XSS prevention (sanitize-html)
- CSRF protection (Mitigated by Bearer token usage instead of cookies)
- Rate limiting (Throttler)
Data Security
- Sensitive data encrypted at rest (AES-256)
- Passwords hashed (bcrypt)
- Secrets in environment variables (not in code)
- Database credentials rotated regularly
- Backup encryption enabled
Access Control
- 4-level RBAC implemented
- Principle of least privilege
- Role-based permissions
- Session timeout (15 minutes)
- Audit logging for all actions
Infrastructure
- Firewall configured
- Intrusion detection (optional)
- Regular security updates
- Vulnerability scanning (
pnpm audit— run before each deploy) - Penetration testing (before go-live)
- Dependency vulnerabilities patched — CASL 6.7.5 (CVE-2026-1774, 2026-02-24)
Consequences
Positive Consequences
- ✅ Secure by Design: ใช้ Industry best practices
- ✅ OWASP Compliant: ครอบคลุม OWASP Top 10
- ✅ Audit Trail: บันทึก Security events ทั้งหมด
- ✅ Token-based: Stateless และ Scalable
- ✅ Defense in Depth: หลายชั้นการป้องกัน
Negative Consequences
- ❌ Complexity: Security measures เพิ่ม Complexity
- ❌ Performance: Encryption/Hashing ใช้ CPU
- ❌ User Friction: Password policy อาจรำคาญผู้ใช้
Mitigation Strategies
- Documentation: เขียน Security guidelines ให้ทีม
- Training: อบรม Security awareness
- Automation: Automated security scans
- Monitoring: Real-time security monitoring
- Frontend Sync: ตรวจสอบว่า
localStorageไม่ถูกดักจับผ่าน XSS ได้ง่าย ๆ เนื่องจากเปลี่ยนจากHTTP-only Cookiesมาเป็นLocalStorage
ADR Review Cycle
Core Principle Review Schedule
- Review Frequency: ทุก 6 เดือน (กุมภาพันธ์ และ สิงหาคม)
- Trigger Events:
- Major version upgrade (v1.9.0, v2.0.0)
- Security vulnerability discovery
- New compliance requirements
- Architecture changes affecting auth
Review Checklist
- JWT configuration still meets security standards
- Password policy alignment with current threats
- Rate limiting effectiveness
- Audit log completeness
- Cross-document dependencies still valid
- Implementation matches documented decisions
- New security best practices to consider
Version Dependency Matrix
| System Version | ADR Version | Required Changes | Status |
|---|---|---|---|
| v1.8.0 - v1.8.5 | ADR-016 v1.0 | Base implementation | ✅ Complete |
| v1.9.0+ | ADR-016 v1.1 | Review JWT expiry times | 📋 Planned |
| v2.0.0+ | ADR-016 v2.0 | Consider session management changes | 📋 Future |
Related ADRs
- ADR-004: RBAC Implementation
- ADR-007: API Design & Error Handling
- ADR-015: Deployment & Infrastructure
References
Document Version: v1.0 Last Updated: 2026-02-24 Next Review: 2026-08-01 (6-month cycle) Version Applicability: LCBP3 v1.8.0+
Change History
| Version | Date | Changes | Author |
|---|---|---|---|
| v1.0 | 2026-02-24 | Initial ADR creation with security strategy | Security Team |
| v1.1 | 2026-04-04 | Added structured templates: Impact Analysis, Gap Linking, Version Dependency, Review Cycle | System Architect |