452 lines
11 KiB
Markdown
452 lines
11 KiB
Markdown
# ADR-016: Security & Authentication Strategy
|
|
|
|
**Status:** ✅ Accepted
|
|
**Date:** 2025-12-01
|
|
**Decision Makers:** Security Team, System Architect
|
|
**Related Documents:** [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md), [ADR-007: API Design](./ADR-007-api-design-error-handling.md)
|
|
|
|
---
|
|
|
|
## Context and Problem Statement
|
|
|
|
LCBP3-DMS จัดการเอกสารสำคัญของโปรเจกต์ ต้องการ Security strategy ที่ครอบคลุม Authentication, Authorization, Data protection, และ Security best practices
|
|
|
|
### ปัญหาที่ต้องแก้:
|
|
|
|
1. **Authentication:** ใช้วิธีไหนในการยืนยันตัวตน
|
|
2. **Session Management:** จัดการ Session อย่างไร
|
|
3. **Password Security:** เก็บ Password อย่างไรให้ปลอดภัย
|
|
4. **Data Encryption:** Encrypt ข้อมูลอย่างไร
|
|
5. **Security Headers:** HTTP Headers ที่ต้องมี
|
|
6. **Input Validation:** ป้องกัน Injection attacks
|
|
7. **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 HTTP-only Cookies**
|
|
|
|
```typescript
|
|
// 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 = 12**
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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**
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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**
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
- [x] JWT authentication with short-lived tokens
|
|
- [x] Password hashing with bcrypt (12 rounds)
|
|
- [x] HTTPS only (TLS 1.3)
|
|
- [x] Security headers (Helmet.js)
|
|
- [x] CORS properly configured
|
|
- [x] Input validation (class-validator)
|
|
- [x] SQL injection prevention (TypeORM)
|
|
- [x] XSS prevention (sanitize-html)
|
|
- [x] CSRF protection (SameSite cookies)
|
|
- [x] Rate limiting (Throttler)
|
|
|
|
### Data Security
|
|
|
|
- [x] Sensitive data encrypted at rest (AES-256)
|
|
- [x] Passwords hashed (bcrypt)
|
|
- [x] Secrets in environment variables (not in code)
|
|
- [x] Database credentials rotated regularly
|
|
- [x] Backup encryption enabled
|
|
|
|
### Access Control
|
|
|
|
- [x] 4-level RBAC implemented
|
|
- [x] Principle of least privilege
|
|
- [x] Role-based permissions
|
|
- [x] Session timeout (15 minutes)
|
|
- [x] Audit logging for all actions
|
|
|
|
### Infrastructure
|
|
|
|
- [x] Firewall configured
|
|
- [x] Intrusion detection (optional)
|
|
- [x] Regular security updates
|
|
- [x] Vulnerability scanning
|
|
- [x] Penetration testing (before go-live)
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
### Positive Consequences
|
|
|
|
1. ✅ **Secure by Design:** ใช้ Industry best practices
|
|
2. ✅ **OWASP Compliant:** ครอบคลุม OWASP Top 10
|
|
3. ✅ **Audit Trail:** บันทึก Security events ทั้งหมด
|
|
4. ✅ **Token-based:** Stateless และ Scalable
|
|
5. ✅ **Defense in Depth:** หลายชั้นการป้องกัน
|
|
|
|
### Negative Consequences
|
|
|
|
1. ❌ **Complexity:** Security measures เพิ่ม Complexity
|
|
2. ❌ **Performance:** Encryption/Hashing ใช้ CPU
|
|
3. ❌ **User Friction:** Password policy อาจรำคาญผู้ใช้
|
|
|
|
### Mitigation Strategies
|
|
|
|
- **Documentation:** เขียน Security guidelines ให้ทีม
|
|
- **Training:** อบรม Security awareness
|
|
- **Automation:** Automated security scans
|
|
- **Monitoring:** Real-time security monitoring
|
|
|
|
---
|
|
|
|
## Related ADRs
|
|
|
|
- [ADR-004: RBAC Implementation](./ADR-004-rbac-implementation.md)
|
|
- [ADR-007: API Design & Error Handling](./ADR-007-api-design-error-handling.md)
|
|
- [ADR-015: Deployment & Infrastructure](./ADR-015-deployment-infrastructure.md)
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
|
- [JWT Best Practices](https://curity.io/resources/learn/jwt-best-practices/)
|
|
- [NestJS Security](https://docs.nestjs.com/security/authentication)
|
|
|
|
---
|
|
|
|
**Last Updated:** 2025-12-01
|
|
**Next Review:** 2026-03-01 (Quarterly review)
|