Auth 도메인 모델
데이터 저장 규칙
- 개인정보 관련 데이터는 private 스키마에 저장되어야 합니다.
- 비개인정보 데이터는 auth 스키마에 저장되어야 합니다.
- 개인정보 포함 여부에 따라 위 규칙을 우선적으로 적용합니다.
- Auth 도메인에서는 Token, Session, AppToken, GesundheitsIDLink 등 사용자 ID와 연결된 데이터는 모두 개인정보로 취급하여 private 스키마에 저장합니다.
- 사용자와 연결되지 않은 설정 데이터(JWK, SecurityPolicy 등)만 auth 스키마에 저장합니다.
- 외부 인증 식별자(
external_identity,external_identity_tokens)는auth스키마에서 관리하며, 공급자 레지스트리(auth.external_identity_provider)와 함께 지역 확장 컨텍스트와 공통으로 운영합니다. - User 엔티티는 User 도메인과 공유되며, 사용자 유형에 따라 데이터가 다른 스키마에 저장됩니다:
- 고객(환자) 사용자 데이터는
private.users테이블에 저장됩니다. - 내부 운영자, 의료진 등 고객이 아닌 사용자 데이터는
operation.users테이블에 저장됩니다.
- 고객(환자) 사용자 데이터는
- 본 문서의 Prisma 스키마 섹션에는 Auth 도메인이 직접 관리하는 엔티티만 정의합니다.
- User 엔티티의 전체 Prisma 스키마 정의는
user/domain-model.md문서를 참조하세요.
1. 엔티티 관계도 (ERD)
참고: User 엔티티는 User 도메인과 공유됩니다. User 엔티티의 상세 정의 및 전체 스키마는 user/domain-model.md를 참조하세요. 위 ERD는 Auth 도메인 관점에서의 주요 관계를 보여줍니다.
2. 엔티티
2.1 JWK
interface JWK {
id: number; // 키 식별자
jwk: JWKPayload; // 공개키 정보
private_key: PrivateKey; // 개인키 정보
status: TokenStatus; // 키 상태 (ACTIVE, REVOKED, EXPIRED 등)
createdAt: Date; // 생성 시간
}
2.2 User (Auth Domain View)
/**
* 사용자 계정 엔티티 (Auth 도메인 책임 필드 중심).
* User 도메인과 공유되는 테이블로, 사용자 유형에 따라 저장 스키마가 달라집니다.
* - 고객(환자) 사용자: `private.users` 테이블
* - 내부 운영자/의료진: `operation.users` 테이블
* User 도메인의 `domain-model.md`에 정의된 Prisma 스키마가 원본입니다.
*/
interface UserAuthView {
id: UserId;
email: Email; // 로그인 ID (Auth 책임)
passwordHash: PasswordHash; // 해시된 비밀번호 (Auth 책임)
status: UserStatus; // 계정 상태 (공유 관리)
userType: UserType; // 사용자 유형 (PATIENT, OPERATOR, ADMIN 등)
failedLoginAttempts: number; // 실패 횟수 (Auth 책임)
lockoutUntil?: Date; // 잠금 만료 (Auth 책임)
lastLoginAt?: Date; // 마지막 로그인 (Auth 책임)
}
/**
* 사용자 유형 (User 도메인과 일치)
*/
enum UserType {
PATIENT = 'PATIENT', // 환자(고객) - private 스키마에 저장
OPERATOR = 'OPERATOR', // 운영자 - operation 스키마에 저장
ADMIN = 'ADMIN', // 관리자 - operation 스키마에 저장
CLINICIAN = 'CLINICIAN', // 의료진 - operation 스키마에 저장
SERVICE_ACCOUNT = 'SERVICE_ACCOUNT' // 서비스 계정 - operation 스키마에 저장
}
/**
* 사용자 계정 상태 (User 도메인과 일치)
*/
enum UserStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
LOCKED = 'LOCKED',
PENDING = 'PENDING',
SUSPENDED = 'SUSPENDED',
BANNED = 'BANNED'
}
2.3 RefreshToken
interface RefreshToken {
id: string;
userId: string;
token: string; // Refresh Token 값 (실제로는 해시 저장 권장)
identityLevel: IdentityLevel; // guest, registered, mfa 등
deviceBinding: string; // 토큰이 발급된 디바이스/앱 토큰 식별자
identityBindings: string[]; // 연동된 인증 수단 목록 (게스트는 빈 배열)
expiresAt: Date;
createdAt: Date;
revokedAt?: Date; // 폐기 시간
}
/**
* 내부 운영자용 RefreshToken
* 구조는 동일하지만 operation 스키마에 저장됨
*/
interface OperationRefreshToken {
// RefreshToken과 동일한 구조
// operation 스키마에 저장
}
2.4 SecurityPolicy
interface SecurityPolicy {
id: string;
passwordPolicy: PasswordPolicy;
tokenPolicy: TokenPolicy;
lockoutPolicy: LockoutPolicy;
mfaPolicy: MfaPolicy;
createdAt: Date;
updatedAt: Date;
}
2.5 AppToken
interface AppToken {
id: string;
appId: string;
token: string;
deviceId: string;
issuedAt: Date;
expiresAt: Date;
lastUsedAt?: Date;
status: TokenStatus;
createdAt: Date;
updatedAt: Date;
}
2.6 Session
type IdentityLevel = 'guest' | 'registered' | 'mfa' | 'privileged';
interface Session {
id: string;
userId: string;
deviceInfo: DeviceInfo;
lastActivityAt: Date;
expiresAt: Date;
data: Record<string, any>;
identityLevel: IdentityLevel; // 세션이 획득한 인증 수준
createdAt: Date;
updatedAt: Date;
}
2.7 GuestLinkSession
interface GuestLinkSession {
id: string;
guestAccountId: string;
linkMethod: 'email' | 'kakao' | 'other';
context: Record<string, any>; // 예: 이메일 주소 마스킹, OAuth state 등
status: 'PENDING' | 'COMPLETED' | 'CANCELLED' | 'EXPIRED';
expiresAt: Date;
createdAt: Date;
completedAt?: Date;
}
2.8 IdentityLevelChange
interface IdentityLevelChange {
id: string;
userId: string;
previousLevel: IdentityLevel;
nextLevel: IdentityLevel;
revokedRefreshTokens: string[]; // 블랙리스트 처리된 토큰 식별자
linkSessionId?: string; // GuestLinkSession.id 참조
occurredAt: Date;
}
2.9 GesundheitsIDLink
interface GesundheitsIDLink {
id: string;
userId: string;
gesundheitsIDHash: string;
metadata?: any;
status: GesundheitsIDLinkStatus;
verifiedAt?: Date;
linkedAt: Date;
unlinkedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
enum GesundheitsIDLinkStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
PENDING_VERIFICATION = 'PENDING_VERIFICATION'
}
2.10 ExternalIdentityProvider
interface ExternalIdentityProvider {
code: string; // 고유 식별자 (예: kakao, apple, google)
displayName: string;
region: AuthProviderRegion; // libs/shared/contracts-auth의 지역 enum 사용
isActive: boolean;
metadata?: Record<string, any>; // 동의 문구, scope 등 구성 값
createdAt: Date;
updatedAt: Date;
}
공급자 레지스트리 운영 절차
- 공급자 코드 정의
kebab-case소문자를 사용하고 지역 접두사는 생략합니다(예:kakao,gesundheitsid).SELECT code FROM auth.external_identity_provider로 중복 여부를 확인합니다.
- 데이터 등록
- 마이그레이션보다는 시드/설정 스크립트를 사용해
auth.external_identity_provider에 레코드를 추가합니다. - 필수 필드:
code,display_name,region,is_active. metadata에는 OAuth scope, 동의 문구, UI 라벨, 약관 링크 등을 JSON으로 기록합니다.
- 마이그레이션보다는 시드/설정 스크립트를 사용해
- 구성 연동
libs/core/config의auth.oauth네임스페이스에 공급자별 설정 값을 추가합니다.- 지역 컨텍스트 라이브러리(예:
libs/feature/auth-kr)에서 신규 공급자를AuthProviderRegistry에 등록하고 TimeMachine/Redis 의존성을 확인합니다.
- 문서 및 태스크 업데이트
- 지역 문서(
domains/<region>/core-domains/auth/...)에 공급자 세부 정보를 추가하고, 본 절차를 링크합니다. - Backlog 태스크에 레지스트리 변경, 테스트 근거(
yarn test:features등), 배포 체크리스트를 기록합니다.
- 지역 문서(
- 검증 및 배포
- 스테이징 환경에서 OAuth 인가/콜백 흐름을 스모크 테스트하고,
auth.external_identity/auth.external_identity_tokens에 예상 레코드가 생성되는지 확인합니다. - 실패 시 회복 전략(Feature flag, Circuit Breaker 설정)을 점검합니다.
- 스테이징 환경에서 OAuth 인가/콜백 흐름을 스모크 테스트하고,
- 비활성화/폐기
- 공급자 종료 시
is_active를false로 업데이트하고, 지역 컨텍스트에서 공급자 모듈을 제거하거나 feature flag로 비활성화합니다. - 외부 토큰 폐기, 감사 로그 기록, 사용자 공지 등의 후속 조치를 수행합니다.
- 공급자 종료 시
2.11 ExternalIdentity
interface ExternalIdentity {
id: string;
userId: string;
providerCode: string; // auth.external_identity_provider.code 참조
externalUserId: EncryptedString; // 외부 공급자 사용자 ID (암호화 저장)
metadata?: Record<string, any>; // 공급자별 보조 정보 (예: displayName, avatarUrl)
status: ExternalIdentityStatus;
linkedAt: Date;
unlinkedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
- Unique constraints enforce ① (`providerCode`, `externalUserId`)로 외부 ID 중복 연결을 차단하고 ② (`providerCode`, `userId`)로 동일 사용자의 중복 공급자 레코드를 방지한다.
enum ExternalIdentityStatus {
ACTIVE = 'ACTIVE',
UNLINKED = 'UNLINKED',
PENDING = 'PENDING'
}
2.12 ExternalIdentityToken
interface ExternalIdentityToken {
id: string;
externalIdentityId: string;
providerCode: string; // auth.external_identity_provider.code 참조
refreshTokenCipher: string; // 암호화된 리프레시 토큰
expiresAt: Date;
lastRotatedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
3. 값 객체
3.1 TokenPayload
// 인터페이스 이름을 AccessTokenPayload 로 변경하고 roles, permissions 추가 고려
// 또는 기존 TokenPayload 에 선택적으로 추가
// -> DtxClaims 로 통합하기로 결정 (아래 DtxClaims 정의 참조)
// interface TokenPayload { ... } // 이 정의는 삭제합니다.
3.2 DeviceInfo
interface DeviceInfo {
deviceId: string;
deviceType: string;
osType: string;
osVersion: string;
browserType?: string;
browserVersion?: string;
appVersion?: string;
}
3.3 DeviceIdentifier
interface DeviceIdentifier {
uuid: string; // 장치 고유 식별자
platform: string; // 플랫폼 정보 (iOS, Android, web)
version: string; // 클라이언트 앱 버전
timestamp: number; // 생성 시간 (Unix timestamp)
}
3.4 PasswordPolicy
interface PasswordPolicy {
minLength: number;
maxLength: number;
requireUppercase: boolean;
requireLowercase: boolean;
requireNumbers: boolean;
requireSpecialChars: boolean;
preventReuseCount: number;
expirationDays: number;
}
3.5 TokenPolicy
interface TokenPolicy {
accessTokenTtlMinutes: number;
refreshTokenTtlDays: number;
rotateRefreshToken: boolean;
}
3.6 LockoutPolicy
interface LockoutPolicy {
maxFailedAttempts: number;
lockoutDurationMinutes: number;
resetCounterAfterMinutes: number;
}
3.7 MfaPolicy
interface MfaPolicy {
required: boolean;
allowedMethods: MfaMethod[];
gracePeriodMinutes: number;
rememberDeviceDays: number;
}
enum MfaMethod {
TOTP = 'TOTP',
SMS = 'SMS',
EMAIL = 'EMAIL',
BIOMETRIC = 'BIOMETRIC'
}
3.8 EmailVerificationType
enum EmailVerificationType {
REGISTRATION = 'REGISTRATION', // 회원가입 이메일 인증
LOGIN = 'LOGIN', // 로그인 이메일 인증
PASSWORD_RESET = 'PASSWORD_RESET' // 비밀번호 재설정 이메일 인증
}
3.9 AppTokenPayload
interface AppTokenPayload {
sub: string; // appId
iat: number; // 발급 시간
exp: number; // 만료 시간
jti: string; // 토큰 고유 ID
deviceId: string; // 암호화된 디바이스 ID
}
3.10 GesundheitsIDMetadata
interface GesundheitsIDMetadata {
lastVerified: Date;
scopes: string[];
idProvider: string;
}
3.11 EncryptedString
/**
* AES GCM 등 표준 방식으로 암호화된 문자열을 표현하는 Value Object.
* 외부 식별자, 리프레시 토큰 등 PII/민감 데이터에 사용합니다.
*/
interface EncryptedString {
cipherText: string; // 암호화된 본문
iv: string; // 초기화 벡터
tag: string; // 인증 태그
version: string; // 암호화 버전
}
4. 집계 (Aggregates)
-
RefreshToken Aggregate (New)
- Root: RefreshToken
-
Session Aggregate
- Root: Session
- Entities: User (Reference)
- Value Objects: DeviceInfo
-
GesundheitsID Aggregate
- Root: GesundheitsIDLink
- Entities: User (Reference)
- Value Objects: GesundheitsIDMetadata
-
ExternalIdentity Aggregate
- Root: ExternalIdentity
- Entities: ExternalIdentityProvider (reference), ExternalIdentityToken (optional)
- Value Objects: EncryptedString
-
AppSecurity Aggregate
- Root: AppToken
- Entities: User (Reference)
- Value Objects: DeviceIdentifier, AppTokenPayload
-
SecurityPolicy Aggregate
- Root: SecurityPolicy
- Value Objects: PasswordPolicy, TokenPolicy, LockoutPolicy, MfaPolicy
-
Token (JWT/JWK) Aggregate (Refined)
- Root: JWK (as Key Management Root)
- Value Objects: TokenPayload, RSAKey, JWKPayload, PrivateKey
참고: User는 Auth 도메인 내 집계의 루트는 아니지만, 다른 집계에서 참조됩니다. User 집계 자체의 관리 책임은 User 도메인에 있습니다.
5. 도메인 서비스
5.1 TokenService
interface TokenService {
// createToken 메서드 파라미터 수정 (userId 등 명시적 전달)
createToken(userId: string, email: string, deviceInfo?: DeviceInfo): Promise<Token>;
validateToken(token: string): Promise<TokenPayload | null>;
revokeToken(token: string): Promise<void>;
revokeUserTokens(userId: string): Promise<void>; // 특정 사용자의 모든 토큰 폐기
refreshToken(refreshToken: string): Promise<Token>;
}
5.2 JWKService
interface JWKService {
createKeyPair(): Promise<JWK>;
getActiveKey(): Promise<JWK>;
getActivePrivateKey(): Promise<PrivateKey>; // 내부용 개인키 반환
rotateKeys(): Promise<void>;
getKeyById(keyId: string): Promise<JWK | null>;
}
5.3 AuthenticationService
interface AuthenticationService {
// 회원가입
register(email: string, password: string, verificationId: string, accessCode: string, agreements: Agreement[], consents: Record<string, boolean>, deviceInfo: DeviceInfo): Promise<User>;
registerWithHashedPassword(email: string, passwordHash: string, verificationId: string, accessCode: string, agreements: Agreement[], consents: Record<string, boolean>, deviceInfo: DeviceInfo): Promise<User>;
// 이메일 인증
sendVerificationEmail(email: string, type: EmailVerificationType, deviceInfo: DeviceInfo): Promise<{ requestId: string, expiresIn: number }>;
verifyEmailCode(email: string, code: string, requestId: string, type: EmailVerificationType): Promise<{ verified: boolean, verificationId: string, expiresAt: Date }>;
// 로그인
login(email: string, password: string, deviceInfo: DeviceInfo): Promise<Token>;
loginWithHashedPassword(email: string, passwordHash: string, deviceInfo: DeviceInfo): Promise<Token>; // 해시된 비밀번호로 로그인
logout(token: string): Promise<void>; // Access Token 기반 로그아웃
logoutSession(sessionId: string): Promise<void>; // 세션 기반 로그아웃
refreshToken(refreshToken: string, deviceInfo: DeviceInfo): Promise<Token>; // 기기 정보 추가
validateToken(token: string): Promise<TokenPayload>; // 반환 타입 Non-nullable로 변경 (유효하지 않으면 에러 throw)
changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void>;
changePasswordWithHashedPassword(userId: string, oldPasswordHash: string, newPasswordHash: string): Promise<void>; // 해시된 비밀번호로 변경
requestPasswordReset(email: string): Promise<void>; // 비밀번호 재설정 요청
resetPassword(resetToken: string, newPassword: string): Promise<void>; // 비밀번호 재설정 실행
resetPasswordWithHashedPassword(resetToken: string, newPasswordHash: string): Promise<void>; // 해시된 새 비밀번호로 재설정
setupMfa(userId: string, method: MfaMethod): Promise<string | { qrCodeUrl: string }>; // 설정 정보 반환 (예: TOTP QR 코드 URL)
verifyMfa(userId: string, code: string): Promise<boolean>;
disableMfa(userId: string, method: MfaMethod): Promise<void>; // MFA 비활성화 추가
// 사용자 상태 관련 메서드 (User 도메인과 협력)
lockAccount(userId: string, reason: string): Promise<void>;
unlockAccount(userId: string, reason: string): Promise<void>;
}
5.4 SecurityPolicyService
interface SecurityPolicyService {
getPolicy(): Promise<SecurityPolicy>;
updatePolicy(policy: Partial<SecurityPolicy>): Promise<SecurityPolicy>;
validatePassword(password: string): Promise<boolean>;
// enforcePasswordPolicy 는 주기적인 작업 또는 이벤트 기반으로 처리될 수 있음
}
5.5 AppTokenService
export interface AppTokenService {
// generateAppToken 파라미터 수정
generateAppToken(deviceId: DeviceIdentifier, appId: string, userId?: string): Promise<string>; // userId 선택적
validateAppToken(token: string): Promise<{ isValid: boolean; payload?: AppTokenPayload }>; // 비동기 처리, payload 반환
// decodeAppToken 제거 (validateAppToken 에서 payload 반환)
revokeAppToken(token: string): Promise<void>; // 앱 토큰 폐기 추가
}
5.6 SessionService
interface SessionService {
createSession(userId: string, deviceInfo: DeviceInfo): Promise<Session>;
getSession(sessionId: string): Promise<Session | null>;
updateSessionActivity(sessionId: string): Promise<void>; // 마지막 활동 시간 갱신
// updateSession 제거 (활동 시간 갱신으로 대체 또는 필요시 재정의)
deleteSession(sessionId: string): Promise<void>;
deleteAllUserSessions(userId: string, excludeCurrentSessionId?: string): Promise<void>; // 특정 사용자 세션 정리
cleanExpiredSessions(): Promise<void>;
}
5.7 GesundheitsIDService
interface GesundheitsIDService {
linkGesundheitsID(userId: string, authCode: string): Promise<GesundheitsIDLink>; // 실제 연동 시 authCode 등 필요
unlinkGesundheitsID(userId: string): Promise<void>;
// validateGesundheitsID 제거 (연동/로그인 과정에서 처리)
initiateGesundheitsIDLogin(): Promise<{ redirectUrl: string; state: string }>; // 로그인 시작 URL 반환
handleGesundheitsIDCallback(code: string, state: string, storedState: string): Promise<Token>; // 콜백 처리 및 로그인
getGesundheitsIDStatus(userId: string): Promise<GesundheitsIDLinkStatus | null>;
// refreshGesundheitsIDData 제거 (주기적 또는 필요시 백그라운드 처리)
}
5.8 AuthService (JWT 중심 서비스 - 이름 변경 고려: JwtService)
interface JwtService { // 이름 변경: AuthService -> JwtService
// createJWT 파라미터 명확화 (TokenPayload 사용)
createToken(payload: TokenPayload, type: 'access' | 'refresh'): Promise<string>;
// authJWT 제거 (TokenService.validateToken 사용)
validateToken(token: string): Promise<TokenPayload>; // TokenService 와 중복되므로 제거 또는 통합 고려
getPublicJWKs(): Promise<JWKPayload[]>; // 공개키 목록만 반환
}
AuthService는 JWT 토큰을 사용한 인증 서비스를 제공합니다. 주요 기능은 다음과 같습니다:
- createJWT: 계정 정보, 토큰 타입, 유효 기간 등을 포함한 JWT 토큰을 생성합니다.
- authJWT: 토큰을 검증하고 디코딩합니다.
- getJWKs: 데이터베이스에서 JWK(JSON Web Key) 목록을 가져옵니다.
참고: 상세 구현 방법은 JWT 인증 구현 문서를 참조하세요.
6. 도메인 이벤트
6.1 토큰 관련 이벤트
interface TokenCreatedEvent {
// tokenId 제거 (실제 토큰 값은 이벤트에 불필요)
userId: string;
type: 'access' | 'refresh'; // 토큰 타입 추가
expiresAt: Date;
sessionId?: string; // 세션 ID 추가
timestamp: Date;
}
interface TokenRefreshedEvent {
// oldTokenId, newTokenId 제거
userId: string;
sessionId?: string;
timestamp: Date;
}
interface TokenRevokedEvent {
// tokenId 제거
userId: string;
type: 'access' | 'refresh' | 'all'; // 폐기된 토큰 타입
reason: string;
sessionId?: string; // 특정 세션 토큰 폐기 시
timestamp: Date;
}
interface JWKRotatedEvent {
oldKeyId: string;
newKeyId: string;
timestamp: Date;
}
6.2 인증 관련 이벤트
interface UserLoggedInEvent { // 이름 변경: UserAuthenticatedEvent -> UserLoggedInEvent
userId: string;
sessionId: string; // 세션 ID 추가
timestamp: Date;
deviceInfo: DeviceInfo;
ipAddress: string;
}
interface UserLoggedOutEvent {
userId: string;
sessionId?: string; // 세션 ID 추가 (세션 기반 로그아웃 시)
// token 제거 (필요시 reason 추가)
timestamp: Date;
}
interface PasswordChangeRequestedEvent { // 추가: 비밀번호 재설정 요청
userId: string;
email: string;
timestamp: Date;
}
interface PasswordChangedEvent {
userId: string;
timestamp: Date;
reason: 'USER_REQUEST' | 'ADMIN_RESET' | 'EXPIRATION'; // 이유 구체화
}
interface EmailVerificationRequestedEvent {
email: string;
type: EmailVerificationType;
requestId: string;
timestamp: Date;
ipAddress: string;
}
interface EmailVerificationCompletedEvent {
email: string;
type: EmailVerificationType;
timestamp: Date;
success: boolean;
verificationId?: string;
}
6.3 세션 관련 이벤트
interface SessionCreatedEvent {
sessionId: string;
userId: string;
deviceInfo: DeviceInfo;
timestamp: Date;
}
interface SessionExpiredEvent {
sessionId: string;
userId: string;
timestamp: Date;
}
interface SessionTerminatedEvent { // 이름 변경: SessionUpdatedEvent -> SessionTerminatedEvent (로그아웃 등)
sessionId: string;
userId: string;
reason: 'USER_LOGOUT' | 'ADMIN_ACTION' | 'NEW_LOGIN'; // 종료 사유
timestamp: Date;
}
6.4 보안 관련 이벤트
interface SecurityPolicyUpdatedEvent {
policyId: string; // 필요 없을 수 있음 (보통 정책은 하나)
updatedFields: string[]; // 변경된 정책 필드 목록
updatedBy: string; // 변경 주체 (관리자 ID)
timestamp: Date;
}
interface AccountLockedEvent {
userId: string;
reason: 'FAILED_LOGIN_ATTEMPTS'; // 이유 구체화
durationMinutes: number; // 잠금 기간 추가
timestamp: Date;
}
interface AccountUnlockedEvent { // 추가: 잠금 해제 이벤트
userId: string;
reason: 'LOCKOUT_EXPIRED' | 'ADMIN_ACTION';
unlockedBy?: string; // 관리자 조치 시
timestamp: Date;
}
6.5 GesundheitsID 관련 이벤트
interface GesundheitsIDLinkedEvent {
userId: string;
gesundheitsIDHash: string; // 해시값 추가
timestamp: Date;
}
interface GesundheitsIDUnlinkedEvent {
userId: string;
gesundheitsIDHash: string;
reason: string;
timestamp: Date;
}
interface GesundheitsIDLoginSucceededEvent {
userId: string;
sessionId: string; // 세션 ID 추가
deviceInfo: DeviceInfo;
ipAddress: string;
timestamp: Date;
}
interface GesundheitsIDLoginFailedEvent {
gesundheitsIDHash?: string; // 실패 시 ID를 모를 수 있음
reason: string;
ipAddress: string;
timestamp: Date;
}
7. 도메인 규칙
본 문서에서는 모델 구조에 대한 설명에 중점을 두고 있으며, 상세한 비즈니스 규칙은 Auth 도메인 비즈니스 규칙 문서를 참조하세요.
주요 규칙 카테고리는 다음과 같습니다:
- 인증 규칙: 로그인, 비밀번호, 계정 잠금, 세션 관리 관련 규칙
- 토큰 규칙: JWT 토큰, 리프레시 토큰, 앱 토큰 관련 규칙
- 앱 보안 규칙: 앱 무결성 검증, 디바이스 ID 관리, 앱 권한 관련 규칙
- GesundheitsID 규칙: GesundheitsID 연결, 로그인, 정보 관리 관련 규칙
- Access Code 검증 규칙: 동일 디바이스 ID에 대한 Access Code 시도 횟수 제한 (시간당 10회, 1시간 제한)
- 사용자 유형 구분 규칙: 사용자 유형(환자/내부 운영자)에 따른 데이터 저장 스키마 분리 규칙
각각에 대한 상세 규칙은 비즈니스 규칙 문서에서 확인할 수 있습니다.
7.1 토큰 관련 상세 규칙
JWT 토큰 구현 및 검증 규칙
-
토큰 생성 규칙
- JWT 토큰은 RS256 알고리즘을 사용하여 서명됩니다.
- Access Token과 Refresh Token은 각각 다른 만료 시간을 가집니다 (비즈니스 규칙 1.5, AUTH_CONSTANTS 참조).
- 토큰 페이로드에는 최소한의 필수 정보(sub, iat, exp 등) 및 필요한 추가 클레임(email, consents 등)이 포함됩니다 (TokenPayload 참조).
- 토큰은 항상 활성화된 최신 JWK를 사용하여 서명됩니다.
-
토큰 검증 규칙
- 토큰의 서명은 발급 시 사용된 JWK의 공개키로 검증합니다.
- 토큰의 만료 시간(
exp), 발급자(iss- AUTH_CONSTANTS.ISSUER), 대상(aud- AUTH_CONSTANTS.AUDIENCE), 토큰 타입(type- access/refresh) 등을 모두 검증합니다. - 토큰 ID(
jti)를 사용하여 한 번 사용된 토큰(예: 비밀번호 재설정 토큰)의 재사용을 방지할 수 있습니다. - Refresh Token은 데이터베이스 또는 안전한 저장소에 기록된 값과 일치하는지 확인하여 탈취된 토큰의 사용을 방지합니다.
- Access Token은 Redis에 저장된 해당 사용자의 최신 토큰과 일치해야 합니다. (다중 디바이스 접속 제한 목적)
-
JWK 관리 규칙
- JWK는 주기적으로 교체되어야 합니다 (비즈니스 규칙 3.1 참조).
- 키 교체 시 이전 키는 일정 기간 유효하게 유지하여 기존 토큰 검증을 지원합니다.
- JWK의 개인키 정보는 외부 노출 없이 안전하게 저장 및 관리되어야 합니다.
-
토큰 저장 및 관리 규칙
- Refresh Token은 사용자별로 안전하게 데이터베이스 등에 저장되어 관리되어야 합니다.
- Access Token은 클라이언트(예: 브라우저 메모리, 앱 변수)에 저장하되, 보안에 유의해야 합니다 (XSS 방지).
- 로그아웃 시 서버 측에서는 해당 Refresh Token을 무효화하고, 클라이언트 측에서는 Access/Refresh Token을 삭제해야 합니다.
- 보안 강화를 위해 Refresh Token 회전(Rotation) 전략 사용을 고려합니다 (비즈니스 규칙 3.1, TokenPolicy 참조).
7.2 비밀번호 암호화 전송 규칙
비밀번호 암호화 및 전송 규칙
-
클라이언트 측 처리 규칙
- 회원 가입과 로그인 시 사용자 비밀번호는 서버로 전송되기 전 반드시 안전하게 처리되어야 합니다.
- 클라이언트에서는 비밀번호를 평문으로 전송하지 않고, SHA-256 단방향 해싱을 적용합니다.
- 해싱할 때 솔트(salt)로 앱 인증 과정에서 받은
appSecret과 디바이스의 고유 ID(DeviceUUID)를 함께 사용합니다. - 해시된 비밀번호는 Base64 인코딩을 적용하여 전송해야 합니다.
- 동일한 원본 비밀번호라도 디바이스가 다르면 해시값이 달라지므로 추가적인 보안을 제공합니다.
-
서버 측 처리 규칙
- 서버는 클라이언트에서 전송한 해시된 비밀번호를 받아 추가적인 보안 처리를 합니다.
- 서버는 이 해시값에 추가적인 암호화 알고리즘(bcrypt, Argon2id 등)을 적용하여 저장합니다.
- 서버는 모든 해시 처리 과정에서 발생한 오류를 적절히 처리하고, 로그에 실제 비밀번호 데이터가 노출되지 않도록 해야 합니다.
- 데이터베이스에는 절대 원본 비밀번호가 저장되어서는 안 됩니다.
-
인증 키 관리 규칙
- 해싱에 사용되는 인증 키(appSecret)는 앱 인증 과정에서 안전하게 교환되어야 합니다.
- 인증 키는 앱토큰 유효 기간과 동일한 주기로 갱신되어야 합니다.
- 키 교환 과정은 안전한 채널(TLS/SSL)을 통해 이루어져야 합니다.
- 클라이언트는 수신한 키를 디바이스의 안전한 저장소에 보관해야 합니다.
-
보안 강화 추가 조치
- 모든 비밀번호 관련 네트워크 트래픽은 TLS/SSL을 통해 이루어져야 합니다.
- 서버는 일정 시간 내 동일한 해시 값이 반복적으로 전송되는 것을 감지하고 차단해야 합니다(재전송 공격 방지).
- 클라이언트에서는 비밀번호 해시를 PIN 코드로 암호화하여 로컬에 저장합니다.
- 서버와 클라이언트는 비밀번호 관련 로그를 남기지 않아야 합니다.
8. 데이터베이스 스키마 (Prisma)
참고: 아래는 Auth 도메인이 직접 관리하는 엔티티에 대한 Prisma 스키마 정의입니다.
User 관련 테이블(users, user_profiles, user_status_history)의 전체 스키마 정의는 user/domain-model.md 문서를 참조하세요.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["auth", "private", "operation"]
}
// 사용자 상태 열거형 (User 도메인과 일치해야 함)
enum UserStatus {
ACTIVE
INACTIVE
LOCKED
PENDING
SUSPENDED
BANNED
}
// 사용자 유형 열거형 (User 도메인과 일치해야 함)
enum UserType {
PATIENT // 환자(고객) - private 스키마에 저장
OPERATOR // 운영자 - operation 스키마에 저장
ADMIN // 관리자 - operation 스키마에 저장
CLINICIAN // 의료진 - operation 스키마에 저장
SERVICE_ACCOUNT // 서비스 계정 - operation 스키마에 저장
}
// 토큰 상태 열거형
enum TokenStatus {
ACTIVE
REVOKED
EXPIRED
}
// GesundheitsID 연결 상태 열거형
enum GesundheitsIDLinkStatus {
ACTIVE
INACTIVE
PENDING_VERIFICATION
}
// MFA 메서드 열거형
enum MfaMethod {
TOTP
SMS
EMAIL
BIOMETRIC
}
// 이메일 인증 타입 열거형
enum EmailVerificationType {
REGISTRATION
LOGIN
PASSWORD_RESET
}
// --- User 모델 정의 제거 ---
// model User { ... } // 전체 정의는 user/domain-model.md 참조
// Refresh Token 모델 (private 스키마 - 고객용)
model RefreshToken {
id String @id @default(uuid())
userId String @map("user_id") // private.users(id) 참조
token String @unique // Refresh Token 값 (해시 저장 권장)
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
revokedAt DateTime? @map("revoked_at") // 폐기 시간
@@index([userId])
@@map("refresh_tokens")
@@schema("private")
}
// Refresh Token 모델 (operation 스키마 - 내부 운영자용)
model OperationRefreshToken {
id String @id @default(uuid())
userId String @map("user_id") // operation.users(id) 참조
token String @unique // Refresh Token 값 (해시 저장 권장)
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
revokedAt DateTime? @map("revoked_at") // 폐기 시간
@@index([userId])
@@map("refresh_tokens")
@@schema("operation")
}
// JWK 모델 (auth 스키마)
model JWK {
id Int @id @default(autoincrement())
jwk Json
privateKey Json? @map("private_key")
status TokenStatus
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
@@map("jwk")
@@schema("auth")
}
// 세션 모델 (private 스키마 - 고객용)
model Session {
id String @id @default(uuid())
userId String @map("user_id") // private.users(id) 참조
deviceInfo Json @map("device_info")
lastActivityAt DateTime @map("last_activity_at")
expiresAt DateTime @map("expires_at")
data Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId])
@@index([expiresAt])
@@map("sessions")
@@schema("private")
}
// 세션 모델 (operation 스키마 - 내부 운영자용)
model OperationSession {
id String @id @default(uuid())
userId String @map("user_id") // operation.users(id) 참조
deviceInfo Json @map("device_info")
lastActivityAt DateTime @map("last_activity_at")
expiresAt DateTime @map("expires_at")
data Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([userId])
@@index([expiresAt])
@@map("sessions")
@@schema("operation")
}
// 앱 토큰 모델 (private 스키마 - 고객용)
model AppToken {
id String @id @default(uuid())
appId String @map("app_id")
userId String? @map("user_id") // private.users(id) 참조 (선택적)
token String @unique
deviceId String @map("device_id")
issuedAt DateTime @map("issued_at")
expiresAt DateTime @map("expires_at")
lastUsedAt DateTime? @map("last_used_at")
status TokenStatus
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
revokedAt DateTime? @map("revoked_at")
@@index([appId])
@@index([deviceId])
@@index([status])
@@map("app_tokens")
@@schema("private")
}
// 앱 토큰 모델 (operation 스키마 - 내부 운영자용)
model OperationAppToken {
id String @id @default(uuid())
appId String @map("app_id")
userId String? @map("user_id") // operation.users(id) 참조 (선택적)
token String @unique
deviceId String @map("device_id")
issuedAt DateTime @map("issued_at")
expiresAt DateTime @map("expires_at")
lastUsedAt DateTime? @map("last_used_at")
status TokenStatus
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
revokedAt DateTime? @map("revoked_at")
@@index([appId])
@@index([deviceId])
@@index([status])
@@map("app_tokens")
@@schema("operation")
}
// 보안 정책 모델 (auth 스키마)
model SecurityPolicy {
id String @id @default(uuid())
passwordPolicy Json @map("password_policy")
tokenPolicy Json @map("token_policy")
lockoutPolicy Json @map("lockout_policy")
mfaPolicy Json @map("mfa_policy")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("security_policies")
@@schema("auth")
}
// GesundheitsID 연결 모델 (private 스키마 - 고객 전용)
model GesundheitsIDLink {
id String @id @default(uuid())
userId String @unique @map("user_id") // private.users(id) 참조
gesundheitsIDHash String @unique @map("gesundheitsid_hash")
metadata Json?
status GesundheitsIDLinkStatus
verifiedAt DateTime? @map("verified_at")
linkedAt DateTime @default(now()) @map("linked_at")
unlinkedAt DateTime? @map("unlinked_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([status])
@@map("gesundheitsid_links")
@@schema("private")
}
참고: Prisma 스키마에서 다른 스키마의 테이블을 직접 @relation으로 연결하는 것은 제한적일 수 있습니다. 위 스키마에서는 Auth 도메인이 관리하는 테이블만 정의하고, User 테이블 참조는 주석으로만 표시했습니다. 실제 구현 시 애플리케이션 레벨에서 관계를 관리하거나, 필요에 따라 Prisma Client 확장 등을 고려해야 할 수 있습니다. 사용자 유형에 따라 적절한 스키마에서 관련 데이터를 조회 및 저장하는 로직이 서비스 레이어에 구현되어야 합니다.
8. MAO 티어/동의 연계 모델 확장
- TokenPayload/Session에
tier(anonymous/guest/member),consent_version,allowed_domains,data_persistence필드를 추가한다. - GuestIssued/GuestUpgraded 이벤트를 반영하기 위해 Token 블랙리스트 및 재발급 이력을 저장하는 Audit/TokenRevocation 엔티티에 tier/consent_version을 기록한다.
- Anonymous/guest 세션은 컨텍스트 저장을 금지하기 위해
data_persistence=none기본값을 사용한다. - 익명 액세스 토큰 발급은 유효한
appToken인증 하에서만 허용하며, 발급된 토큰의 기본 클레임은tier=anonymous,allowed_domains=["faq","docs-public","agent-help-public"],data_persistence=none으로 설정한다.
9. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-03-16 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-04-22 | bok@weltcorp.com | Consent 모델을 UserConsent로 변경하여 동의 항목 확장성 개선 |
| 0.3.0 | 2025-04-29 | bok@weltcorp.com | 동의(Consent) 관련 모델을 Consent 도메인으로 이동 |
| 0.4.0 | 2025-04-29 | bok@weltcorp.com | 약관(Term) 관련 모델을 Term 도메인으로 이동 |
| 0.5.0 | 2025-11-26 | bok@weltcorp.com | MAO 라우팅을 위한 tier/consent_version/allowed_domains/data_persistence 필드 및 guest 승격 시 토큰 폐기/재발급 이력 반영 |
| 0.6.0 | 2025-11-26 | bok@weltcorp.com | 익명 토큰 발급을 appToken 인증으로 제한하고 기본 클레임 설정을 모델에 반영 |
| 0.5.0 | 2025-05-07 | bok@weltcorp.com | 사용자 유형 분리에 따른 스키마 저장 요구사항 추가: 환자는 'private' 스키마, 내부 운영자는 'operation' 스키마에 저장 |
| 0.6.0 | 2025-05-15 | bok@weltcorp.com | EmailVerificationType enum 추가 및 이메일 인증 관련 서비스/이벤트 확장 |