토큰 위임 보안
개요
User Token Delegation Pattern은 중간 서비스가 사용자를 대신하여 토큰을 관리하는 패턴으로, 편의성을 제공하지만 동시에 추가적인 보안 위험을 수반합니다. 이 문서는 토큰 위임 시스템의 보안 위험과 대응 방안을 설명합니다.
현재 적용 프로젝트
dta-wide-mcp:
- User Token Delegation Pattern 구현
- 기본적인 토큰 암호화 (계획 중)
- 메모리 기반 토큰 저장 (현재)
- 401 에러 기반 토큰 갱신 (구현됨)
- 감사 로깅 (기본 수준 구현됨)
보안 강화 로드맵:
- Phase 1 (완료): 기본 토큰 위임 구현
- Phase 2 (진행 중): 토큰 암호화 및 보안 로깅 강화
- Phase 3 (계획): 고급 이상 탐지 및 자동 대응
보안 위험 분석
1. 토큰 저장 위험
위험 요소:
- 중간 서비스에서 사용자 토큰의 평문 저장
- 메모리 덤프를 통한 토큰 노출
- 로그 파일에 토큰 정보 기록
영향도: 높음 - 사용자 계정 완전 탈취 가능
대응 방안:
// 토큰 암호화 저장
class SecureTokenStorage {
private readonly encryptionKey: string;
async storeToken(userId: string, token: string): Promise<void> {
const encryptedToken = await this.encrypt(token);
const hashedUserId = this.hashUserId(userId);
// 암호화된 토큰만 저장
this.storage.set(hashedUserId, {
token: encryptedToken,
timestamp: Date.now(),
checksum: this.calculateChecksum(encryptedToken)
});
}
private async encrypt(data: string): Promise<string> {
const cipher = crypto.createCipher('aes-256-gcm', this.encryptionKey);
return cipher.update(data, 'utf8', 'hex') + cipher.final('hex');
}
}
2. 토큰 전송 위험
위험 요소:
- 네트워크 통신 중 토큰 가로채기
- 중간자 공격(MITM)
- 로그 시스템에 토큰 노출
영향도: 중간 - 세션 하이재킹 가능
대응 방안:
// 안전한 토큰 전송
class SecureTokenTransmission {
async transmitToken(token: string, endpoint: string): Promise<void> {
// HTTPS 강제 사용
if (!endpoint.startsWith('https://')) {
throw new Error('HTTPS required for token transmission');
}
// 토큰을 헤더에만 포함, 로그에서 제외
const headers = {
'Authorization': `Bearer ${token}`,
'X-Request-ID': this.generateRequestId()
};
// 로그에서 Authorization 헤더 제외
this.logger.log('API request', {
endpoint,
headers: this.sanitizeHeaders(headers)
});
}
private sanitizeHeaders(headers: any): any {
const sanitized = { ...headers };
if (sanitized.Authorization) {
sanitized.Authorization = '[REDACTED]';
}
return sanitized;
}
}
3. 권한 상승 위험
위험 요소:
- 중간 서비스가 사용자보다 높은 권한 획득
- 토큰 스코프 확장 공격
- 서비스 계정과 사용자 토큰 혼용
영향도: 높음 - 권한 경계 위반
대응 방안:
// 권한 제한 및 검증
class TokenScopeValidator {
private readonly allowedScopes: Set<string>;
async validateTokenScope(token: string, requestedAction: string): Promise<boolean> {
const tokenPayload = this.decodeToken(token);
const userScopes = new Set(tokenPayload.scopes || []);
// 요청된 작업에 필요한 최소 권한 확인
const requiredScopes = this.getRequiredScopes(requestedAction);
for (const scope of requiredScopes) {
if (!userScopes.has(scope)) {
this.logger.warn('Insufficient scope for action', {
userId: tokenPayload.sub,
requestedAction,
requiredScope: scope,
userScopes: Array.from(userScopes)
});
return false;
}
}
return true;
}
private getRequiredScopes(action: string): string[] {
const scopeMap = {
'read:profile': ['user:read'],
'update:profile': ['user:write'],
'read:sensitive': ['user:read', 'sensitive:read']
};
return scopeMap[action] || [];
}
}
4. 세션 하이재킹 위험
위험 요소:
- 토큰 재사용 공격
- 세션 고정 공격
- 동시 세션 관리 부재
영향도: 중간 - 사용자 세션 탈취
대응 방안:
// 세션 보안 관리
class SecureSessionManager {
private readonly activeSessions = new Map<string, SessionInfo>();
async createSession(userId: string, token: string, metadata: SessionMetadata): Promise<string> {
const sessionId = this.generateSecureSessionId();
// 기존 세션 무효화 (단일 세션 정책)
await this.invalidateExistingSessions(userId);
const sessionInfo: SessionInfo = {
userId,
token: await this.encryptToken(token),
createdAt: Date.now(),
lastAccessAt: Date.now(),
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent,
fingerprint: this.generateFingerprint(metadata)
};
this.activeSessions.set(sessionId, sessionInfo);
// 세션 생성 감사 로그
this.auditLogger.log('SESSION_CREATED', {
userId,
sessionId,
ipAddress: metadata.ipAddress,
userAgent: metadata.userAgent
});
return sessionId;
}
async validateSession(sessionId: string, metadata: SessionMetadata): Promise<boolean> {
const session = this.activeSessions.get(sessionId);
if (!session) {
return false;
}
// 세션 만료 확인
if (this.isSessionExpired(session)) {
await this.invalidateSession(sessionId);
return false;
}
// 디바이스 핑거프린트 검증
const currentFingerprint = this.generateFingerprint(metadata);
if (session.fingerprint !== currentFingerprint) {
this.auditLogger.warn('SESSION_FINGERPRINT_MISMATCH', {
userId: session.userId,
sessionId,
expectedFingerprint: session.fingerprint,
actualFingerprint: currentFingerprint
});
return false;
}
// 마지막 접근 시간 업데이트
session.lastAccessAt = Date.now();
return true;
}
}
보안 모범 사례
1. 토큰 생명주기 관리
class TokenLifecycleManager {
private readonly maxTokenAge = 24 * 60 * 60 * 1000; // 24시간
private readonly refreshThreshold = 30 * 60 * 1000; // 30분
async manageTokenLifecycle(): Promise<void> {
const expiringSessions = await this.findExpiringSessions();
for (const session of expiringSessions) {
try {
// 토큰 갱신 시도
const newToken = await this.refreshToken(session.refreshToken);
await this.updateSessionToken(session.id, newToken);
this.auditLogger.log('TOKEN_REFRESHED', {
userId: session.userId,
sessionId: session.id
});
} catch (error) {
// 갱신 실패시 세션 무효화
await this.invalidateSession(session.id);
this.auditLogger.error('TOKEN_REFRESH_FAILED', {
userId: session.userId,
sessionId: session.id,
error: error.message
});
}
}
}
private async findExpiringSessions(): Promise<SessionInfo[]> {
const now = Date.now();
const threshold = now + this.refreshThreshold;
return Array.from(this.activeSessions.values())
.filter(session => session.expiresAt <= threshold);
}
}
2. 감사 로깅 및 모니터링
class SecurityAuditLogger {
async logSecurityEvent(event: SecurityEvent): Promise<void> {
const auditEntry = {
id: uuidv4(),
timestamp: new Date().toISOString(),
eventType: event.type,
userId: event.userId,
sessionId: event.sessionId,
ipAddress: event.ipAddress,
userAgent: event.userAgent,
details: event.details,
riskLevel: this.calculateRiskLevel(event)
};
// 구조화된 로깅
this.logger.log('SECURITY_EVENT', auditEntry);
// 고위험 이벤트는 즉시 알림
if (auditEntry.riskLevel === 'HIGH') {
await this.sendSecurityAlert(auditEntry);
}
// 패턴 분석을 위한 저장
await this.storeAuditEntry(auditEntry);
}
private calculateRiskLevel(event: SecurityEvent): 'LOW' | 'MEDIUM' | 'HIGH' {
const riskFactors = [
this.checkUnusualLocation(event.ipAddress),
this.checkUnusualTime(event.timestamp),
this.checkMultipleFailures(event.userId),
this.checkSuspiciousUserAgent(event.userAgent)
];
const riskScore = riskFactors.filter(Boolean).length;
if (riskScore >= 3) return 'HIGH';
if (riskScore >= 2) return 'MEDIUM';
return 'LOW';
}
}
3. 이상 행위 탐지
class AnomalyDetector {
private readonly behaviorPatterns = new Map<string, UserBehaviorPattern>();
async detectAnomalies(userId: string, activity: UserActivity): Promise<AnomalyResult> {
const userPattern = this.behaviorPatterns.get(userId) || this.createDefaultPattern();
const anomalies: Anomaly[] = [];
// 비정상적인 접근 시간 패턴
if (this.isUnusualAccessTime(activity.timestamp, userPattern.accessTimes)) {
anomalies.push({
type: 'UNUSUAL_ACCESS_TIME',
severity: 'MEDIUM',
details: `Access at ${new Date(activity.timestamp).toISOString()}`
});
}
// 비정상적인 IP 주소
if (!userPattern.knownIpAddresses.has(activity.ipAddress)) {
anomalies.push({
type: 'UNKNOWN_IP_ADDRESS',
severity: 'HIGH',
details: `New IP address: ${activity.ipAddress}`
});
}
// 과도한 API 호출
const recentCalls = await this.getRecentApiCalls(userId, 300000); // 5분
if (recentCalls.length > userPattern.averageCallsPerMinute * 10) {
anomalies.push({
type: 'EXCESSIVE_API_CALLS',
severity: 'HIGH',
details: `${recentCalls.length} calls in 5 minutes`
});
}
// 패턴 업데이트
this.updateUserPattern(userId, activity);
return {
userId,
anomalies,
riskScore: this.calculateRiskScore(anomalies)
};
}
private calculateRiskScore(anomalies: Anomaly[]): number {
return anomalies.reduce((score, anomaly) => {
const severityWeight = {
'LOW': 1,
'MEDIUM': 3,
'HIGH': 5
};
return score + severityWeight[anomaly.severity];
}, 0);
}
}
4. 토큰 무효화 메커니즘
class TokenRevocationManager {
private readonly revokedTokens = new Set<string>();
private readonly suspiciousUsers = new Set<string>();
async revokeToken(token: string, reason: RevocationReason): Promise<void> {
const tokenHash = this.hashToken(token);
this.revokedTokens.add(tokenHash);
// 토큰 무효화 로깅
this.auditLogger.log('TOKEN_REVOKED', {
tokenHash,
reason: reason.type,
details: reason.details,
timestamp: new Date().toISOString()
});
// 의심스러운 활동인 경우 사용자 플래그
if (reason.type === 'SUSPICIOUS_ACTIVITY') {
this.suspiciousUsers.add(reason.userId);
await this.revokeAllUserTokens(reason.userId);
}
// 토큰 블랙리스트에 추가
await this.addToBlacklist(tokenHash, reason);
}
async isTokenRevoked(token: string): Promise<boolean> {
const tokenHash = this.hashToken(token);
// 로컬 캐시 확인
if (this.revokedTokens.has(tokenHash)) {
return true;
}
// 분산 블랙리스트 확인
return await this.checkDistributedBlacklist(tokenHash);
}
async revokeAllUserTokens(userId: string): Promise<void> {
const userSessions = await this.getUserSessions(userId);
for (const session of userSessions) {
await this.revokeToken(session.token, {
type: 'USER_SECURITY_INCIDENT',
userId,
details: 'All tokens revoked due to security incident'
});
}
// 사용자에게 보안 알림 발송
await this.sendSecurityNotification(userId, 'ALL_TOKENS_REVOKED');
}
}
보안 설정 및 구성
1. 환경별 보안 설정
interface SecurityConfig {
tokenEncryption: {
algorithm: string;
keyRotationInterval: number;
keyDerivationRounds: number;
};
sessionManagement: {
maxConcurrentSessions: number;
sessionTimeout: number;
idleTimeout: number;
};
auditLogging: {
logLevel: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
retentionPeriod: number;
sensitiveDataMasking: boolean;
};
anomalyDetection: {
enabled: boolean;
riskThreshold: number;
autoBlockEnabled: boolean;
};
}
const securityConfigs: Record<string, SecurityConfig> = {
production: {
tokenEncryption: {
algorithm: 'aes-256-gcm',
keyRotationInterval: 7 * 24 * 60 * 60 * 1000, // 7일
keyDerivationRounds: 100000
},
sessionManagement: {
maxConcurrentSessions: 1,
sessionTimeout: 24 * 60 * 60 * 1000, // 24시간
idleTimeout: 30 * 60 * 1000 // 30분
},
auditLogging: {
logLevel: 'INFO',
retentionPeriod: 90 * 24 * 60 * 60 * 1000, // 90일
sensitiveDataMasking: true
},
anomalyDetection: {
enabled: true,
riskThreshold: 5,
autoBlockEnabled: true
}
},
development: {
// 개발 환경 설정 (보안 수준 완화)
tokenEncryption: {
algorithm: 'aes-256-gcm',
keyRotationInterval: 24 * 60 * 60 * 1000, // 1일
keyDerivationRounds: 10000
},
sessionManagement: {
maxConcurrentSessions: 5,
sessionTimeout: 8 * 60 * 60 * 1000, // 8시간
idleTimeout: 60 * 60 * 1000 // 1시간
},
auditLogging: {
logLevel: 'DEBUG',
retentionPeriod: 7 * 24 * 60 * 60 * 1000, // 7일
sensitiveDataMasking: false
},
anomalyDetection: {
enabled: false,
riskThreshold: 10,
autoBlockEnabled: false
}
}
};
2. 보안 헤더 및 미들웨어
class SecurityMiddleware {
async applySecurityHeaders(req: Request, res: Response, next: NextFunction): Promise<void> {
// HTTPS 강제
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// 콘텐츠 타입 스니핑 방지
res.setHeader('X-Content-Type-Options', 'nosniff');
// XSS 보호
res.setHeader('X-XSS-Protection', '1; mode=block');
// 클릭재킹 방지
res.setHeader('X-Frame-Options', 'DENY');
// 리퍼러 정책
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// CSP 설정
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'");
next();
}
async validateTokenSecurity(req: Request, res: Response, next: NextFunction): Promise<void> {
const token = this.extractToken(req);
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
// 토큰 무효화 확인
if (await this.tokenRevocationManager.isTokenRevoked(token)) {
return res.status(401).json({ error: 'Token revoked' });
}
// 토큰 형식 검증
if (!this.isValidTokenFormat(token)) {
return res.status(401).json({ error: 'Invalid token format' });
}
next();
}
}
보안 테스트 및 검증
1. 보안 테스트 시나리오
describe('Token Delegation Security Tests', () => {
describe('Token Storage Security', () => {
it('should encrypt tokens before storage', async () => {
const plainToken = 'test-access-token';
await tokenManager.setupUserSession('user123', { accessToken: plainToken });
const storedData = tokenManager.getStoredData('user123');
expect(storedData.accessToken).not.toBe(plainToken);
expect(storedData.accessToken).toMatch(/^[a-f0-9]+$/); // 암호화된 형태
});
it('should not log sensitive token data', async () => {
const logSpy = jest.spyOn(logger, 'log');
await tokenManager.setupUserSession('user123', {
accessToken: 'sensitive-token'
});
const logCalls = logSpy.mock.calls;
const hasTokenInLogs = logCalls.some(call =>
JSON.stringify(call).includes('sensitive-token')
);
expect(hasTokenInLogs).toBe(false);
});
});
describe('Session Security', () => {
it('should invalidate session on suspicious activity', async () => {
const sessionId = await sessionManager.createSession('user123', token, metadata);
// 의심스러운 활동 시뮬레이션
await anomalyDetector.reportAnomaly('user123', {
type: 'MULTIPLE_FAILED_ATTEMPTS',
severity: 'HIGH'
});
const isValid = await sessionManager.validateSession(sessionId, metadata);
expect(isValid).toBe(false);
});
});
describe('Token Revocation', () => {
it('should immediately revoke compromised tokens', async () => {
const token = 'compromised-token';
await tokenRevocationManager.revokeToken(token, {
type: 'SECURITY_BREACH',
details: 'Token compromised'
});
const isRevoked = await tokenRevocationManager.isTokenRevoked(token);
expect(isRevoked).toBe(true);
});
});
});
2. 보안 메트릭 및 알림
class SecurityMetricsCollector {
async collectSecurityMetrics(): Promise<SecurityMetrics> {
return {
activeTokens: await this.countActiveTokens(),
revokedTokensToday: await this.countRevokedTokens(24 * 60 * 60 * 1000),
suspiciousActivities: await this.countSuspiciousActivities(),
failedAuthAttempts: await this.countFailedAuthAttempts(),
anomalyDetections: await this.countAnomalyDetections(),
securityIncidents: await this.countSecurityIncidents()
};
}
async checkSecurityThresholds(metrics: SecurityMetrics): Promise<void> {
// 비정상적인 토큰 무효화율
if (metrics.revokedTokensToday > metrics.activeTokens * 0.1) {
await this.sendAlert('HIGH_TOKEN_REVOCATION_RATE', metrics);
}
// 과도한 의심스러운 활동
if (metrics.suspiciousActivities > 100) {
await this.sendAlert('HIGH_SUSPICIOUS_ACTIVITY', metrics);
}
// 보안 사고 발생
if (metrics.securityIncidents > 0) {
await this.sendAlert('SECURITY_INCIDENT_DETECTED', metrics);
}
}
}
관련 문서
- 인증 패턴 - 전체 인증 패턴 개요
- 토큰 관리 아키텍처 - 토큰 관리 구현
- 세션 관리 - 세션 관리 보안
- 통합 패턴 - 서비스 간 통합 보안