본문으로 건너뛰기

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;
}
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;
}

공급자 레지스트리 운영 절차

  1. 공급자 코드 정의
    • kebab-case 소문자를 사용하고 지역 접두사는 생략합니다(예: kakao, gesundheitsid).
    • SELECT code FROM auth.external_identity_provider로 중복 여부를 확인합니다.
  2. 데이터 등록
    • 마이그레이션보다는 시드/설정 스크립트를 사용해 auth.external_identity_provider에 레코드를 추가합니다.
    • 필수 필드: code, display_name, region, is_active.
    • metadata에는 OAuth scope, 동의 문구, UI 라벨, 약관 링크 등을 JSON으로 기록합니다.
  3. 구성 연동
    • libs/core/configauth.oauth 네임스페이스에 공급자별 설정 값을 추가합니다.
    • 지역 컨텍스트 라이브러리(예: libs/feature/auth-kr)에서 신규 공급자를 AuthProviderRegistry에 등록하고 TimeMachine/Redis 의존성을 확인합니다.
  4. 문서 및 태스크 업데이트
    • 지역 문서(domains/<region>/core-domains/auth/...)에 공급자 세부 정보를 추가하고, 본 절차를 링크합니다.
    • Backlog 태스크에 레지스트리 변경, 테스트 근거(yarn test:features 등), 배포 체크리스트를 기록합니다.
  5. 검증 및 배포
    • 스테이징 환경에서 OAuth 인가/콜백 흐름을 스모크 테스트하고, auth.external_identity/auth.external_identity_tokens에 예상 레코드가 생성되는지 확인합니다.
    • 실패 시 회복 전략(Feature flag, Circuit Breaker 설정)을 점검합니다.
  6. 비활성화/폐기
    • 공급자 종료 시 is_activefalse로 업데이트하고, 지역 컨텍스트에서 공급자 모듈을 제거하거나 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 토큰을 사용한 인증 서비스를 제공합니다. 주요 기능은 다음과 같습니다:

  1. createJWT: 계정 정보, 토큰 타입, 유효 기간 등을 포함한 JWT 토큰을 생성합니다.
  2. authJWT: 토큰을 검증하고 디코딩합니다.
  3. 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 도메인 비즈니스 규칙 문서를 참조하세요.

주요 규칙 카테고리는 다음과 같습니다:

  1. 인증 규칙: 로그인, 비밀번호, 계정 잠금, 세션 관리 관련 규칙
  2. 토큰 규칙: JWT 토큰, 리프레시 토큰, 앱 토큰 관련 규칙
  3. 앱 보안 규칙: 앱 무결성 검증, 디바이스 ID 관리, 앱 권한 관련 규칙
  4. GesundheitsID 규칙: GesundheitsID 연결, 로그인, 정보 관리 관련 규칙
  5. Access Code 검증 규칙: 동일 디바이스 ID에 대한 Access Code 시도 횟수 제한 (시간당 10회, 1시간 제한)
  6. 사용자 유형 구분 규칙: 사용자 유형(환자/내부 운영자)에 따른 데이터 저장 스키마 분리 규칙

각각에 대한 상세 규칙은 비즈니스 규칙 문서에서 확인할 수 있습니다.

7.1 토큰 관련 상세 규칙

JWT 토큰 구현 및 검증 규칙

  1. 토큰 생성 규칙

    • JWT 토큰은 RS256 알고리즘을 사용하여 서명됩니다.
    • Access Token과 Refresh Token은 각각 다른 만료 시간을 가집니다 (비즈니스 규칙 1.5, AUTH_CONSTANTS 참조).
    • 토큰 페이로드에는 최소한의 필수 정보(sub, iat, exp 등) 및 필요한 추가 클레임(email, consents 등)이 포함됩니다 (TokenPayload 참조).
    • 토큰은 항상 활성화된 최신 JWK를 사용하여 서명됩니다.
  2. 토큰 검증 규칙

    • 토큰의 서명은 발급 시 사용된 JWK의 공개키로 검증합니다.
    • 토큰의 만료 시간(exp), 발급자(iss - AUTH_CONSTANTS.ISSUER), 대상(aud - AUTH_CONSTANTS.AUDIENCE), 토큰 타입(type - access/refresh) 등을 모두 검증합니다.
    • 토큰 ID(jti)를 사용하여 한 번 사용된 토큰(예: 비밀번호 재설정 토큰)의 재사용을 방지할 수 있습니다.
    • Refresh Token은 데이터베이스 또는 안전한 저장소에 기록된 값과 일치하는지 확인하여 탈취된 토큰의 사용을 방지합니다.
    • Access Token은 Redis에 저장된 해당 사용자의 최신 토큰과 일치해야 합니다. (다중 디바이스 접속 제한 목적)
  3. JWK 관리 규칙

    • JWK는 주기적으로 교체되어야 합니다 (비즈니스 규칙 3.1 참조).
    • 키 교체 시 이전 키는 일정 기간 유효하게 유지하여 기존 토큰 검증을 지원합니다.
    • JWK의 개인키 정보는 외부 노출 없이 안전하게 저장 및 관리되어야 합니다.
  4. 토큰 저장 및 관리 규칙

    • Refresh Token은 사용자별로 안전하게 데이터베이스 등에 저장되어 관리되어야 합니다.
    • Access Token은 클라이언트(예: 브라우저 메모리, 앱 변수)에 저장하되, 보안에 유의해야 합니다 (XSS 방지).
    • 로그아웃 시 서버 측에서는 해당 Refresh Token을 무효화하고, 클라이언트 측에서는 Access/Refresh Token을 삭제해야 합니다.
    • 보안 강화를 위해 Refresh Token 회전(Rotation) 전략 사용을 고려합니다 (비즈니스 규칙 3.1, TokenPolicy 참조).

7.2 비밀번호 암호화 전송 규칙

비밀번호 암호화 및 전송 규칙

  1. 클라이언트 측 처리 규칙

    • 회원 가입과 로그인 시 사용자 비밀번호는 서버로 전송되기 전 반드시 안전하게 처리되어야 합니다.
    • 클라이언트에서는 비밀번호를 평문으로 전송하지 않고, SHA-256 단방향 해싱을 적용합니다.
    • 해싱할 때 솔트(salt)로 앱 인증 과정에서 받은 appSecret과 디바이스의 고유 ID(DeviceUUID)를 함께 사용합니다.
    • 해시된 비밀번호는 Base64 인코딩을 적용하여 전송해야 합니다.
    • 동일한 원본 비밀번호라도 디바이스가 다르면 해시값이 달라지므로 추가적인 보안을 제공합니다.
  2. 서버 측 처리 규칙

    • 서버는 클라이언트에서 전송한 해시된 비밀번호를 받아 추가적인 보안 처리를 합니다.
    • 서버는 이 해시값에 추가적인 암호화 알고리즘(bcrypt, Argon2id 등)을 적용하여 저장합니다.
    • 서버는 모든 해시 처리 과정에서 발생한 오류를 적절히 처리하고, 로그에 실제 비밀번호 데이터가 노출되지 않도록 해야 합니다.
    • 데이터베이스에는 절대 원본 비밀번호가 저장되어서는 안 됩니다.
  3. 인증 키 관리 규칙

    • 해싱에 사용되는 인증 키(appSecret)는 앱 인증 과정에서 안전하게 교환되어야 합니다.
    • 인증 키는 앱토큰 유효 기간과 동일한 주기로 갱신되어야 합니다.
    • 키 교환 과정은 안전한 채널(TLS/SSL)을 통해 이루어져야 합니다.
    • 클라이언트는 수신한 키를 디바이스의 안전한 저장소에 보관해야 합니다.
  4. 보안 강화 추가 조치

    • 모든 비밀번호 관련 네트워크 트래픽은 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.02025-03-16bok@weltcorp.com최초 작성
0.2.02025-04-22bok@weltcorp.comConsent 모델을 UserConsent로 변경하여 동의 항목 확장성 개선
0.3.02025-04-29bok@weltcorp.com동의(Consent) 관련 모델을 Consent 도메인으로 이동
0.4.02025-04-29bok@weltcorp.com약관(Term) 관련 모델을 Term 도메인으로 이동
0.5.02025-11-26bok@weltcorp.comMAO 라우팅을 위한 tier/consent_version/allowed_domains/data_persistence 필드 및 guest 승격 시 토큰 폐기/재발급 이력 반영
0.6.02025-11-26bok@weltcorp.com익명 토큰 발급을 appToken 인증으로 제한하고 기본 클레임 설정을 모델에 반영
0.5.02025-05-07bok@weltcorp.com사용자 유형 분리에 따른 스키마 저장 요구사항 추가: 환자는 'private' 스키마, 내부 운영자는 'operation' 스키마에 저장
0.6.02025-05-15bok@weltcorp.comEmailVerificationType enum 추가 및 이메일 인증 관련 서비스/이벤트 확장