Auth API 엔드포인트
📖 라이브 명세: dta-wide-api v2 Swagger UI (dev) — 본 페이지는 보조 자료이며, 컨트롤러의
@ApiOperation/@ApiResponse데코레이터가 진실 원본입니다. drift 시 Swagger UI를 우선합니다. (api-reference 개요에 전체 환경/버전 표.)
목차
- 관련 문서
- 접근 권한 매트릭스
- 앱 인증 API
- Access Code 검증 API
- 약관 관리 API
- 게스트 인증 API
- 인증 핵심 기능 API
- Eid 인증 및 연동
- EU eID 회원가입
- EU Native 2FA 회원가입
- EU 로그인
- 내부 운영자 인증 API
- 오류 코드
- 변경 이력
관련 문서
- Agreements 도메인 엔드포인트: /domains/common/core-domains/agreements/endpoints
- Access Code 도메인 엔드포인트: /domains/common/supporting-domains/access-code/endpoints
접근 권한 매트릭스
| 엔드포인트 | System Admin | IAM Admin | Service Account | Regular User | 비고 |
|---|---|---|---|---|---|
| POST /v1/auth/app/challenge | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/app/complete-challenge | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/access-code/validate | ✓ | ✓ | ✓ | ✓ | |
| GET /v1/auth/agreements | ✓ | ✓ | ✓ | ✓ | App/Access Token |
| POST /v1/auth/email/verification-code | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/email/verify | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/register | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/guest/register | ✓ | ✓ | ✓ | ✓ | App Token, identityLevel=guest 발급 |
| POST /v1/auth/guest/link/init | ✓ | ✓ | ✓ | ✓ | 게스트 토큰 + 동일 디바이스 필요 |
| POST /v1/auth/guest/link/complete | ✓ | ✓ | ✓ | ✓ | Step-up 인증 완료, 게스트 토큰 필수 |
| POST /v1/auth/guest/link/cancel | ✓ | ✓ | ✓ | ✓ | 진행 중인 Step-up 세션 취소 |
| POST /v1/auth/login | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/token/refresh | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/token/validate | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/logout | ✓ | ✓ | ✓ | ✓ | |
| POST /v1/auth/2fa/enable | ✓ | ✓ | ✘ | ✓ | |
| POST /v1/auth/2fa/verify | ✓ | ✓ | ✘ | ✓ | |
| POST /v1/auth/2fa/disable | ✓ | ✓ | ✘ | ✓ | |
| POST /v1/auth/token/anonymous | ✓ | ✓ | ✓ | ✓ | appToken 필수(익명 토큰 발급), 기본 클레임 tier=anonymous/allowed_domains/data_persistence |
| GET /v1/auth/sessions | ✓ | ✓ | ✓ | 자신만 | |
DELETE /v1/auth/sessions/{id} | ✓ | ✓ | ✘ | 자신만 | |
| POST /v1/auth/eid/link/initialize | ✓ | ✓ | ✘ | 자신만 | |
| POST /v1/auth/eid/link/complete | ✓ | ✓ | ✘ | 자신만 | |
| GET /v1/auth/eid/link | ✓ | ✓ | ✘ | 자신만 | |
| POST /v1/auth/eid/unlink | ✓ | ✓ | ✘ | 자신만 | |
| POST /v1/auth/eid/login/initialize | ✓ | ✓ | ✘ | ✓ | |
| POST /v1/auth/eid/login/complete | ✓ | ✓ | ✘ | ✓ | |
| POST /v2/auth/register/eid/initialize | ✓ | ✓ | ✘ | ✓ | EU eID 가입 10.1 — app token |
| POST /v2/auth/register/eid/complete | ✓ | ✓ | ✘ | ✓ | EU eID 가입 10.2 — app token, 응답 LoginV2ResponseDto(login/eid 와 동일) |
| POST /v2/auth/register/native | ✓ | ✓ | ✘ | ✓ | EU Native 2FA 가입 11 — app token, 공개키만(단일 endpoint, self-POP 제거), 응답 LoginV2ResponseDto(login/native 와 동일) (Plan 209) |
| POST /v2/auth/login/eid/initialize | ✓ | ✓ | ✘ | ✓ | EU eID 로그인 12 — FlexibleAuthGuard (app token 또는 user access token) |
| POST /v2/auth/login/eid/complete | ✓ | ✓ | ✘ | ✓ | EU eID 로그인 12 — first-login/재로그인/새 기기 분기, LoginV2ResponseDto |
| POST /v2/auth/login/eid/link/initialize | ✓ | ✓ | ✘ | 자신만 | EU eID link 시작 12 — JwtAuthGuard (Plan 209 — auth/login/eid/* 표준) |
| POST /v2/auth/login/eid/link/complete | ✓ | ✓ | ✘ | 자신만 | EU eID link 완료 12 — JwtAuthGuard, eID↔native 배타성(requested method wins) |
| GET /v2/auth/login/eid/link | ✓ | ✓ | ✘ | 자신만 | EU eID link 상태 조회 12 — JwtAuthGuard |
| POST /v2/auth/login/eid/unlink | ✓ | ✓ | ✘ | 자신만 | EU eID unlink 12 — JwtAuthGuard |
| POST /v2/auth/login/native/initialize | ✓ | ✓ | ✘ | ✓ | EU Native 2FA 로그인 12 — 재로그인 nonce 발급(user access token) |
| POST /v2/auth/login/native/complete | ✓ | ✓ | ✘ | ✓ | EU Native 2FA 로그인 12 — keyless(app token, 공개키 upsert) / 재로그인(서명), LoginV2ResponseDto |
| POST /v2/auth/login/native/link | ✓ | ✓ | ✘ | 자신만 | EU Native 2FA link 12 — method→native, OTP step-up + 새 공개키, eID 배타 만료 (Plan 209) |
| POST /v2/auth/user-cycle/activate | ✓ | ✓ | ✘ | ✓ | EU 서비스 활성화 — JwtAuthGuard, uci 가 박힌 ActivateServiceResponseDto(LoginV2ResponseDto 상속) 재발급 |
| GET /v2/auth/user-cycle/state | ✓ | ✓ | ✘ | ✓ | EU 서비스 상태 조회 — JwtAuthGuard |
| POST /v1/auth/operation/register | ✓ | ✓ | ✘ | ✘ | 내부 운영자 등록 |
DELETE /v1/auth/operation/user/{id} | ✓ | ✘ | ✘ | ✘ | 내부 운영자 삭제 |
| POST /v1/auth/operation/login | ✓ | ✓ | ✓ | ✘ | 내부 운영자 로그인 |
| POST /v1/auth/operation/logout | ✓ | ✓ | ✓ | ✘ | 내부 운영자 로그아웃 |
| POST /v1/auth/operation/email/verification-code | ✓ | ✓ | ✓ | ✘ | 내부 운영자 이메일 인증 |
| POST /v1/auth/operation/email/verify | ✓ | ✓ | ✓ | ✘ | 내부 운영자 이메일 확인 |
| POST /v1/auth/operation/token/refresh | ✓ | ✓ | ✓ | ✘ | 내부 운영자 토큰 갱신 |
| POST /v1/auth/operation/token/validate | ✓ | ✓ | ✓ | ✘ | 내부 운영자 토큰 검증 |
| DELETE /v1/auth/registration/abort | ✓ | ✓ | ✓ | ✓ | 등록 프로세스 중단 및 캐시 정리 |
| DELETE /v1/auth/me | ✓ | ✓ | ✓ | 자신만 | 계정 비활성화 (탈퇴) |
참고:
- ✓: 접근 가능
- ✘: 접근 불가
- (범위 내): 할당된 조직/팀 범위 내에서만 접근 가능
- (자신만): 자신의 데이터에만 접근 가능
- App/Access Token: 앱 토큰 또는 사용자 액세스 토큰으로 접근 가능
앱 인증 API
📌 기술 구현 문서: 앱 인증 시스템 구현 | 앱 보안 인증 시스템 기술 명세
📌 인증 방식: 앱 인증은 챌린지-응답 메커니즘을 통해 앱 무결성을 검증하는 보안 인증 방식입니다. 핵심 엔드포인트는 challenge(챌린지 요청), complete-challenge(챌린지 완료 및 토큰 발급), verify-token(토큰 검증) 입니다.
앱 보안 인증 API는 모바일 앱과 백엔드 서버 간의 안전한 통신을 위한 인증 메커니즘을 제공합니다. 이 API는 Device ID 기반 인증과 시간 기반 OTP 챌린지 인증을 통해 클라이언트 앱의 무결성을 검증합니다.
중요: 앱 인증 프로세스는 사용자 로그인 전에 가장 먼저 수행되어야 하는 단계입니다. 앱 인증을 통해 발급받은
appToken은 사용자가 로그인하여accessToken을 발급받기 전까지 필요한 모든 API 호출(Access Code 검증, 약관 조회/동의, 이메일 인증, 회원가입, 로그인 등)에 사용됩니다. 이는 유효한 클라이언트 앱에서의 요청임을 먼저 검증한 후, 사용자 인증을 진행하는 2단계 인증 구조입니다.
참고: 본 문서에 포함된 모든 시간 관련 필드는 13자리 Unix 타임스탬프(밀리초) 형식을 사용합니다. 시간 데이터 처리에 대한 자세한 내용은 데이터 타입 가이드라인을 참조하세요.
앱 인증 토큰 사용 흐름
-
앱 보안 인증
- 디바이스 정보를 이용해 챌린지 요청(
/auth/app/challenge) 및 응답(/auth/app/complete-challenge)을 통해 appToken 발급 - 모든 API 요청에
Authorization: Bearer {appToken}헤더 포함
- 디바이스 정보를 이용해 챌린지 요청(
-
사용자 미인증 상태에서의 API 호출
- 앱 토큰만으로 접근 가능한 모든 API에 appToken 사용
- 예시: Access Code 검증, 약관 목록 조회 및 동의, 이메일 인증, 회원가입, 로그인 등
ℹ️ 참고: 사용자 인증 후 토큰 사용
- 로그인 성공 시 accessToken과 refreshToken 발급
- 이후 사용자 인증이 필요한 API는
Authorization: Bearer {accessToken}헤더 사용- accessToken 만료 시 refreshToken을 이용해 새로운 토큰 발급
- 자세한 내용은 사용자 인증 섹션 참조
2.1 앱 보안 인증 API
2.1.1 챌린지 요청 API
- HTTP 메서드: POST
- 경로: /auth/app/challenge
- Content-Type: application/json
요청 (Request)
{
"deviceId": {
"uuid": "e8e53358-c282-4f69-89c4-bae40f0b7587",
"platform": "iOS",
"version": "1.2.0",
"timestamp": 1714523700000
},
"deviceIdHash": "AMoVT5hzKN9A3OCz_PjZdzEJYZL5K8pY0FpOYgpKa7U"
}
참고:
deviceIdHash는deviceId객체를 JSON 문자열로 직렬화한 후 SHA-256 해싱하여 생성합니다. 해시 생성 시 앱 시크릿을 솔트로 추가하여 보안을 강화합니다.(Salt는 추후에 개발 예정)
응답 (Response)
- 성공 응답 (200 OK)
{
"challenge": "randomBase64EncodedChallengeString",
"serverTimestamp": 1714523700000,
"nonce": "randomNonceString"
}
- 오류 응답 (400 Bad Request)
{
"code": 2009,
"message": "INVALID_DEVICE_ID",
"detail": "디바이스 ID가 유효하지 않습니다."
}
2.1.2 챌린지 완료 및 토큰 발급 API
- HTTP 메서드: POST
- 경로: /auth/app/complete-challenge
- Content-Type: application/json
요청 (Request)
{
"hashedDeviceId": "f1a23bc45d67e8f9a0b1c2d3e4f56a78b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4",
"encryptedChallenge": "base64EncodedEncryptedChallenge",
"nonce": "nonceFromPreviousRequest"
}
참고:
encryptedChallenge는 챌린지 응답 API에서 받은 챌린지 문자열을 시간 기반 암호화 키로 암호화한 것입니다. 암호화 키는 타임윈도우, 디바이스 ID 해시, 논스를 조합하여 생성합니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"appToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"appSecret": "secureRandomAppSecretString",
"expiresAt": 1714527300000,
"device": {
"id": "device_123",
"uuid": "e8e53358-c282-4f69-89c4-bae40f0b7587",
"platform": "iOS",
"version": "1.2.0",
"createdAt": 1714523700000,
"lastActiveAt": 1714523700000
}
}
- 오류 응답 (401 Unauthorized)
{
"code": 2010,
"message": "CHALLENGE_VERIFICATION_FAILED",
"detail": "챌린지 검증에 실패했습니다."
}
2.2 사용자 인증
2.2.1 로그인 API
- HTTP 메서드: POST
- 경로: /auth/login
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
응답 (Response)
- 성공 응답 (200 OK)
{
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "ACCESS_TOKEN",
"expiresIn": 1800,
"issuedAt": 1714523700000
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "REFRESH_TOKEN",
"expiresIn": 604800,
"issuedAt": 1714523700000
}
],
"user": {
"id": "c1e7c5cf-6fc2-4f4f-8e2f-9b8a3c5e2d1b",
"email": "user@example.com",
"questionnaireBundleId": "be7c5cf-6fc2-4f4f-8e2f-9b85e2d1b",
"suspensionEndAt": 1968886400000,
"createdAt": 1678886400000
},
"userCycle": {
"id": "d2f8e5c1-9b3a-4f8c-8e2f-1b9a3c5e2d7c",
"status": "ACTIVE",
"startedAt": 1678886400000,
"count": 3
},
"profile": {
"language": "de-DE",
"timezone": {
"id": "Europe/Berlin",
"offsetInMinutes": 60
},
"userName": "John Doe"
},
"roles": ["healthcare.patientUser", "content.editor"],
"permissions": ["sleep.log.create", "sleep.log.read", "sleep.goal.update"],
"agreements": [
{
"versionId": "some-version-id",
"type": "CONSENT",
"isRequired": false,
"title": "Datenverarbeitung zur Verbesserung",
"text": "Ich erlaube die Verarbeitung der zu der dauerhaften Gewährleistung der technischen Funktionsfähigkeit...",
"detailsUrl": "https://www.weltcorp.com",
"isAgreed": false,
"agreedAt": 1699981800000
}
]
}
2.2.2 사용자 정보 조회 API
- HTTP 메서드: GET
- 경로: /auth/user/
{userId} - Headers:
- Authorization: Bearer
{accessToken}
- Authorization: Bearer
응답 예시
{
"id": "user_123",
"email": "user@example.com",
"name": "홍길동",
"status": "ACTIVE",
"createdAt": 1711929600000,
"updatedAt": 1714523700000,
"lastLoginAt": 1714524600000
}
2.3 역할 관리 API
- HTTP 메서드: GET
- 경로: /auth/roles
- Headers:
- Authorization: Bearer
{accessToken}
- Authorization: Bearer
응답 예시
{
"items": [
{
"id": "role_123",
"name": "ADMIN",
"description": "시스템 관리자",
"permissions": ["USER_MANAGE", "ROLE_MANAGE"],
"createdAt": 1711929600000,
"updatedAt": 1714523700000
}
],
"total": 1,
"page": 1,
"pageSize": 10,
"totalPages": 1
}
2.4 사용자 역할 할당 API
- HTTP 메서드: POST
- 경로: /auth/user/
{userId}/roles - Headers:
- Authorization: Bearer
{accessToken} - Content-Type: application/json
- Authorization: Bearer
요청 (Request)
{
"roleIds": ["role_123"]
}
응답 (Response)
- 성공 응답 (200 OK)
{
"userId": "user_123",
"roleIds": ["role_123"],
"assignedAt": 1714524600000,
"assignedBy": "admin_456"
}
Access Code 검증 API
📌 기술 구현 문서: Access Code 검증 구현 가이드 | Access Code 명세
📌 구현 순서: 앱 보안 인증 API 다음으로 구현해야 하는 중요한 기능입니다.
Access Code는 회원가입 시 사용자의 접근 권한을 검증하는 데 사용되는 16자리 코드입니다. 회원가입 전에 반드시 유효한 Access Code 검증이 필요합니다.
3.1 Access Code 검증 API
- HTTP 메서드: POST
- 경로: /auth/access-code/validate
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"accessCode": "ABCD1234EFGH5678"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"valid": true, // 검증 성공
"accessCodeId": "ac_123",
"expiresAt": 1718949600000, // Unix Timestamp (milliseconds)
"isUsed": false,
"metadata": {
"remainingAttempts": 10 // 성공 시 남은 시도 횟수 (최대값)
}
}
- 실패 응답 - 유효하지 않은 코드 (400 Bad Request)
{
"code": 3001,
"message": "INVALID_ACCESS_CODE",
"detail": "유효하지 않은 Access Code입니다.",
"metadata": {
"remainingAttempts": 9 // 실패 후 남은 시도 횟수
}
}
- 실패 응답 - 만료된 코드 (400 Bad Request)
{
"code": 3003,
"message": "ACCESS_CODE_EXPIRED",
"detail": "만료된 Access Code입니다.",
"metadata": {
"remainingAttempts": 9 // 실패 후 남은 시도 횟수
}
}
- 실패 응답 - 이미 사용된 코드 (409 Conflict)
{
"code": 3002,
"message": "ACCESS_CODE_ALREADY_USED",
"detail": "이미 사용된 Access Code입니다.",
"metadata": {
"remainingAttempts": 9 // 실패 후 남은 시도 횟수
}
}
- 실패 응답 - 시도 횟수 제한 초과 (429 Too Many Requests)
{
"code": 3007,
"message": "CODE_BLOCKED",
"detail": "잘못된 시도 횟수 초과로 인해 Access Code가 일시적으로 차단되었습니다.",
"metadata": {
"remainingLockoutSeconds": 3599 // 잠금 해제까지 남은 시간(초)
}
}
약관 관리 API
📌 기술 구현 문서: 약관 관리 구현 가이드 | 약관 관리 명세
📌 구현 순서: Access Code 검증 API 다음으로 구현해야 하는 기능입니다.
📌 참고: 약관 관리 프로세스에 대한 자세한 내용은 약관 도메인 엔드포인트 문서를 참조하세요.
경로 변경 안내: 약관 관리 API는 기존
/auth/terms에서/terms로 경로가 변경되었습니다.
약관 관리 API는 서비스 이용을 위한 약관의 버전 관리와 활성화 상태 관리, 다국어 지원 및 사용자 동의 이력 관리를 담당합니다.
게스트/익명 인증 API
📌 설계 문서: 게스트 Step-up 인증 및
identityLevel확장 설계📌 요약: 게스트/익명 인증 API는 약관 동의만으로 제한 계정(guest 또는 anonymous)을 생성하고, 추가 인증을 통해 정규 사용자로 승격시키는 Step-up 플로우를 제공합니다. 모든 요청은 App Token 검증을 통과한 디바이스에서 호출되어야 하며, 게스트/익명 토큰은 제한된 allowed_domains·data_persistence를 가진다.
4.0 익명 토큰 발급
- HTTP 메서드:
POST - 경로:
/v1/auth/token/anonymous(정책에 맞게 조정 가능) - 인증:
Authorization: Bearer {appToken} - 설명: 사용자가 로그인하지 않은 상태에서 anonymous tier 토큰을 발급한다. 기본 클레임은
tier=anonymous,allowed_domains=["faq","docs-public","agent-help-public"],data_persistence=none을 강제한다. - 제약:
- appToken 필수(어떤 앱이든 유효한 appToken이면 가능), 사용자 인증 토큰 없이 호출
- 발급된 토큰은 FAQ/정보성 에이전트만 호출 가능, 컨텍스트/툴 호출 제한
4.1 게스트 온보딩
- HTTP 메서드:
POST - 경로:
/v1/auth/guest/register - 인증:
Authorization: Bearer {appToken},User-Agent헤더 필수 - 설명: 이메일·소셜 인증 없이 필수 약관 동의와 디바이스 검증만으로
identityLevel=guest토큰 페어를 발급합니다.region은 User-Agent 헤더의locale에서 자동 추출됩니다.
요청 헤더
Authorization: Bearer {appToken}
User-Agent: dha-sleep-app/2.3.1 (iOS; iOS 17.5; iPhone14,5; AppStore; locale/ko-KR)
Content-Type: application/json
User-Agent에서 자동 추출되는 정보:
region: locale에서 추출 (예:locale/ko-KR→region: "KR")platform,appVersion,osVersion,model: 디바이스 정보
요청 본문
{
"agreements": [
{ "versionId": "9f8a7b6c-5d4e-3f2g-1h0i-j9k8l7m6n5o4", "isAgreed": true },
{ "versionId": "marketing-agreement-version-id", "isAgreed": false }
],
"deviceUuid": "e8e53358-c282-4f69-89c4-bae40f0b7587",
"profile": {
"language": "ko-KR",
"timezone": {
"id": "Asia/Seoul",
"offsetInMinutes": 540
}
}
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
agreements | array | Yes | 약관 동의 목록 (정식 회원가입과 동일한 형식) |
deviceUuid | string | Yes | 디바이스 UUID (앱 인증 API의 deviceId.uuid와 동일한 값 사용) |
profile | object | Yes | 사용자 프로필 정보 (정식 회원가입과 동일한 형식) |
profile.language | string | Yes | 사용자 언어 설정 (예: ko-KR, de-DE) |
profile.timezone.id | string | Yes | 시간대 ID (예: Asia/Seoul, Europe/Berlin) |
profile.timezone.offsetInMinutes | number | Yes | UTC 기준 오프셋 (분 단위) |
응답 본문 (201 Created)
{
"success": true
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
success | boolean | Yes | 게스트 등록 성공 여부 |
ℹ️ 참고: 게스트 등록 API는 계정 생성만 수행합니다. 토큰 발급 및 상세 정보(
user,userCycle,profile등)는 게스트 로그인 API를 통해 획득합니다.
⚠️ 제약
- 게스트 등록 후 별도의 로그인 API(
/v1/auth/guest/login)를 통해 토큰을 받아야 합니다.- 게스트 토큰은 최초 디바이스에만 유효합니다. 다른 디바이스에서 사용하려면 Step-up 후 재로그인해야 합니다.
- 필수 약관 미동의 시
1103 REQUIRED_TERMS_NOT_AGREED오류가 반환됩니다.
4.1.1 게스트 로그인
- HTTP 메서드:
POST - 경로:
/v1/auth/guest/login - 인증:
Authorization: Bearer {appToken},User-Agent헤더 필수 - 설명: 이미 등록된 게스트 계정으로 다시 로그인합니다. 디바이스 UUID로 기존 게스트 계정을 식별하고 새로운 토큰 페어를 발급합니다.
요청 헤더
Authorization: Bearer {appToken}
User-Agent: dha-sleep-app/2.3.1 (iOS; iOS 17.5; iPhone14,5; AppStore; locale/ko-KR)
Content-Type: application/json
요청 본문
{
"deviceUuid": "e8e53358-c282-4f69-89c4-bae40f0b7587"
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
deviceUuid | string | Yes | 디바이스 UUID (게스트 등록 시 사용한 것과 동일한 값) |
응답 본문 (200 OK)
{
"tokens": [
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "ACCESS_TOKEN",
"expiresIn": 900,
"issuedAt": 1714523700000
},
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"type": "REFRESH_TOKEN",
"expiresIn": 43200,
"issuedAt": 1714523700000
}
],
"user": {
"id": "guest_a1b2c3d4-5e6f-7g8h-9i0j-k1l2m3n4o5p6",
"identityLevel": "GUEST",
"createdAt": 1714520000000,
"expiresAt": 1714606400000
},
"userCycle": {
"id": "d2f8e5c1-9b3a-4f8c-8e2f-1b9a3c5e2d7c",
"status": "ACTIVE",
"startedAt": 1714520000000,
"count": 1,
"treatmentDurationDays": -1
},
"profile": {
"language": "ko-KR",
"timezone": {
"id": "Asia/Seoul",
"offsetInMinutes": 540
}
},
"roles": ["guest.user"],
"permissions": ["sleep.log.create", "sleep.log.read", "content.read"],
"agreements": [...]
}
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
tokens | array | Yes | 인증 토큰 배열 (access + refresh) |
user | object | Yes | 사용자(게스트) 계정 정보 |
user.id | string | Yes | 사용자 계정 ID |
user.identityLevel | string | Yes | 신원 수준 (게스트의 경우 GUEST) |
user.createdAt | number | Yes | 계정 생성일 (Unix timestamp ms) |
user.expiresAt | number | No | 세션 만료일 (Unix timestamp ms) |
userCycle | object | Yes | 사용자 주기 정보 (정식 사용자 login API의 userCycle과 동일) |
userCycle.id | string | Yes | 사용자 주기 ID |
userCycle.status | string | Yes | 주기 상태 (ACTIVE) |
userCycle.startedAt | number | Yes | 주기 시작일 (Unix timestamp ms) |
userCycle.count | number | Yes | 총 주기 개수 (게스트는 항상 1) |
userCycle.treatmentDurationDays | number | Yes | 치료 기간 (일수). 게스트는 -1 (무제한) |
profile | object | Yes | 프로필 정보 |
roles | string[] | Yes | 역할 목록 |
permissions | string[] | Yes | 권한 목록 |
agreements | array | Yes | 동의한 약관 목록 |
⚠️ 오류 응답
404 GUEST_ACCOUNT_NOT_FOUND: 해당 디바이스로 등록된 게스트 계정이 없음410 GUEST_ACCOUNT_EXPIRED: 게스트 계정이 만료됨410 GUEST_ACCOUNT_UPGRADED: 정식 사용자로 업그레이드되어 게스트 로그인 불가
4.2 Step-up 인증 준비
- HTTP 메서드:
POST - 경로:
/v1/auth/guest/link/init - 인증:
Authorization: Bearer {guestAccessToken} - 설명: 이메일 등 외부 자격 연동을 위한 임시
linkSessionId를 발급합니다. 게스트 토큰과 동일한 디바이스에서만 호출 가능합니다.
요청 본문
{
"method": "email",
"payload": {
"email": "guest@example.com",
"language": "de"
}
}
응답 본문 (200 OK)
{
"linkSessionId": "link_01J4V0JMSF8PM6N2V2SVS1ZPAB",
"expiresAt": 1724300400000,
"method": "email"
}
⚠️ 제약
- 동일 디바이스 검증 실패 시
401 DEVICE_MISMATCH_FOR_STEP_UP오류를 반환합니다.- 이미 정규 사용자로 승격된 계정은
409 IDENTITY_LEVEL_ALREADY_REGISTERED오류가 발생합니다.
4.3 Step-up 인증 완료
- HTTP 메서드:
POST - 경로:
/v1/auth/guest/link/complete - 인증:
Authorization: Bearer {guestRefreshToken}(Refresh Token 기반 재발급 요청) - 설명:
linkSessionId와 외부 자격 증명을 검증하고, 새로운identityLevel=registered토큰을 발급합니다. 성공 시 기존 게스트 토큰은 블랙리스트 처리됩니다.
요청 본문
{
"linkSessionId": "link_01J4V0JMSF8PM6N2V2SVS1ZPAB",
"verificationCode": "483912"
}
응답 본문 (200 OK)
{
"accessToken": "eyJhbGciOi...",
"refreshToken": "def50200...",
"identityLevel": "registered",
"bindings": [
{
"type": "email",
"value": "guest@example.com"
}
],
"revokedRefreshTokens": ["def501ab-guest-refresh"]
}
⚠️ 정책
- 검증 실패 3회 초과 시 게스트 계정이
LOCKED상태로 전환되고429 STEP_UP_TOO_MANY_ATTEMPTS가 반환됩니다.- 성공 시
auth.identity-level.changed이벤트가 발행되어 IAM/Plan/Group이 재계산을 수행합니다.
4.4 Step-up 인증 취소
- HTTP 메서드:
POST - 경로:
/v1/auth/guest/link/cancel - 인증:
Authorization: Bearer {guestAccessToken} - 설명: 진행 중인 Step-up 세션을 취소하고 이메일·OAuth 임시 리소스를 정리합니다.
요청 본문
{
"linkSessionId": "link_01J4V0JMSF8PM6N2V2SVS1ZPAB",
"reason": "USER_ABORT"
}
응답 본문 (204 No Content)
⚠️ 정책
- 이미 완료되었거나 만료된 세션을 취소하면
404 LINK_SESSION_NOT_FOUND가 반환됩니다.- 취소 후 재시도 시 새로운
linkSessionId를 발급해야 합니다.
인증 핵심 기능 API
📌 구현 순서: 앱 보안 인증, Access Code 검증, 약관 관리 API가 구현된 후 진행합니다.
5.1 회원가입 프로세스
📌 기술 구현 문서: 회원가입 프로세스 구현 가이드 | 인증 서비스 명세
회원가입은 다음과 같은 단계로 진행됩니다:
- Access Code 검증: 사용자가 제출한 16자리 Access Code의 유효성을 검증합니다.
- 통합 동의 항목 조회: 회원가입 화면에 표시해야 할 필수 약관 및 선택적 동의 항목 목록과 상태를 조회합니다.
- 이메일 인증 코드 발송: 사용자가 입력한 이메일로 6자리 OTP 번호를 발송합니다.
- 이메일 인증 코드 확인: 사용자가 수신한 OTP 번호를 입력하여 이메일 유효성을 검증합니다.
- 회원가입: Access Code 검증, 약관 동의 정보와 인증이 완료된 이메일을 사용하여 회원가입을 진행합니다. 이때 클라이언트는 동의한 약관 정보를 함께 전달합니다.
중요: 회원가입 프로세스의 모든 단계는 사용자 로그인 전이므로, 각 API 호출 시 앱 보안 인증을 통해 발급받은
appToken을Authorization: Bearer {appToken}헤더에 포함하여 요청해야 합니다. 회원가입 성공 후에는 사용자accessToken이 발급되며, 이후 API 호출에는 이 accessToken을 사용합니다.
중요: 회원가입은 반드시 유효한 Access Code 검증과 이메일 인증이 모두 완료된 후에만 가능하며, 필수 약관 동의 정보가 포함되어야 합니다. 필수 약관에 동의하지 않은 경우
REQUIRED_TERMS_NOT_AGREED오류가 발생하며, 인증되지 않은 이메일로 회원가입을 시도할 경우EMAIL_NOT_VERIFIED오류가 발생합니다. 이메일 인증은 완료 후 30분 동안 유효합니다.
5.1.1 약관 목록 조회
- HTTP 메서드: GET
- 경로: /terms
- Headers:
- Accept-Language: ko-KR (기본값) 또는 en-US
- Authorization: Bearer
{appToken 또는 accessToken}
- 용도: 시스템의 모든 약관(활성/비활성 등)을 필터링하여 목록 형태로 조회할 때 사용합니다. (예: 관리자 페이지) 또는 특정 타입의 약관만 필요할 때 사용합니다. 회원가입/온보딩 등 여러 종류의 동의 항목이 필요한 화면에는 아래의 통합 동의 항목 조회 API 사용을 권장합니다.
응답 (Response)
- 성공 응답 (200 OK): (기존과 동일)
{
"terms": [
{
"id": 1,
"version": "1.0.0",
"title": "서비스 이용약관",
"type": "service",
"required": true,
"createdAt": 1672531200000,
"updatedAt": 1672531200000
}
]
}
5.1.2 통합 동의 항목 조회
- HTTP 메서드: GET
- 경로: /v1/auth/agreements
- Headers:
- Accept-Language: de-DE (기본값), ko-KR, en-US 등
- Authorization: Bearer
{appToken 또는 accessToken}
- 용도: 특정 시점(예: 회원가입, 로그인 후 첫 진입)에 사용자에게 반드시 제시하고 동의받아야 하는 필수 약관과 함께 표시될 선택적 동의 항목 및 해당 사용자의 현재 동의 상태를 한 번에 조회하기 위해 사용됩니다. 클라이언트는 이 응답을 사용하여 독일어 예시 이미지와 같은 동의 화면을 구성할 수 있습니다.
참고: 이 API는 약관 도메인과 동의 도메인의 정보를 통합적으로 제공하지만, 주로 인증 프로세스(회원가입, 로그인)에서 사용되므로 인증 도메인 아래에 위치합니다. 약관 및 동의 관련 자세한 정보는 각각 Agreements 도메인 문서를 참조하세요.
응답 (Response)
- 성공 응답 (200 OK):
{
"items": [
{
"type": "TERMS", // 약관 항목
"key": "9f8a7b6c-5d4e-3f2g-1h0i-j9k8l7m6n5o4", // 약관 버전 ID (UUID 형식)
"isRequired": true, // 필수 동의 여부
"version": "1.0.0", // 현재 활성 버전
"title": "Nutzungsbedingungen", // 약관 이름 번역 (agreements_translations.name)
"text": "Ich stimme den Nutzungsbedingungen gemäß der DiGAV 4 Abs. 2 zu.", // 현지화된 텍스트
"detailsUrl": "/terms/service/v1.0.0/de", // 약관 상세 내용 URL
"orderIndex": 0, // UI 표시 순서 (0부터 시작, 작은 값이 먼저 표시)
"isAgreed": false // 사용자의 이 버전에 대한 동의 여부 (기존 동의 이력 기반)
},
{
"type": "CONSENT", // 선택적 동의 항목
"key": "DATA_PROCESSING_IMPROVEMENT", // 동의 목적 키 (ConsentPurpose)
"isRequired": false,
"title": "Datenverarbeitung zur Verbesserung", // 동의 항목 이름 번역
"text": "Ich erlaube die Verarbeitung der zu der dauerhaften Gewährleistung der technischen Funktionsfähigkeit...", // 현지화된 텍스트
"detailsUrl": null, // 관련 설명 페이지 URL (선택적)
"orderIndex": 1, // UI 표시 순서 (0부터 시작, 작은 값이 먼저 표시)
"isAgreed": false // 사용자의 현재 이 목적에 대한 동의 상태 (Consent 정보 기반)
}
// ... 다른 필요한 약관 또는 동의 항목 추가 ...
]
}
- 오류 응답 (401 Unauthorized - 토큰 오류)
{
"code": 2050,
"message": "INVALID_APP_TOKEN",
"detail": "토큰이 유효하지 않습니다"
}
- 오류 응답 (500 Internal Server Error)
{
"code": 2000,
"message": "SERVER_ERROR",
"detail": "서버 내부 오류"
}
5.1.3 이메일 인증 코드 발송
- HTTP 메서드: POST
- 경로: /auth/email/verification-code
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
중요: 이 API는 회원가입, 로그인, 비밀번호 재설정 등 다양한 인증 프로세스에서 사용되는 이메일 인증 코드 발송 API입니다. 인증 목적에 따라
type필드를 사용합니다. REGISTRATION 타입의 경우 이미 가입된 이메일에 대해 오류가 반환됩니다.
요청 (Request)
{
"email": "user@example.com",
"type": "REGISTRATION"
}
| 필드 | 타입 | 설명 | 필수 |
|---|---|---|---|
email | string | 인증 코드를 받을 이메일 주소 | Yes |
type | string | 이메일 인증 타입 (REGISTRATION, LOGIN, PASSWORD_RESET) | Yes |
응답 (Response)
- 성공 응답 (200 OK)
{
"success": true,
"email": "user@example.com",
"expiresIn": 300,
"requestId": "req_123",
"type": "REGISTRATION"
}
- 오류 응답 (409 Conflict - 이미 가입된 이메일, type이 REGISTRATION인 경우)
{
"code": 2020,
"message": "EMAIL_ALREADY_EXISTS",
"detail": "이미 사용 중인 이메일입니다."
}
- 오류 응답 (422 Unprocessable Entity - 가입되지 않은 이메일, type이 LOGIN 또는 PASSWORD_RESET인 경우)
{
"code": 2027,
"message": "UNREGISTERED_EMAIL",
"detail": "가입되지 않은 이메일입니다."
}
참고: 이메일로 6자리 OTP 번호가 발송됩니다.
expiresIn은 인증 코드의 유효 시간(초)입니다.requestId는 인증 코드 확인 시 필요합니다.
5.1.4 이메일 인증 코드 확인
- HTTP 메서드: POST
- 경로: /auth/email/verify
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"email": "user@example.com",
"code": "123456",
"requestId": "req_123",
"type": "REGISTRATION"
}
| 필드 | 타입 | 설명 | 필수 |
|---|---|---|---|
email | string | 인증 코드를 받은 이메일 주소 | Yes |
code | string | 이메일로 받은 6자리 인증 코드 | Yes |
requestId | string | 코드 발송 요청 시 받은 요청 ID | Yes |
type | string | 이메일 인증 타입 (REGISTRATION, LOGIN, PASSWORD_RESET) | Yes |
응답 (Response)
- 성공 응답 (200 OK)
{
"valid": true,
"email": "user@example.com",
"verificationId": "ver_456",
"verifiedAt": 1711011900000,
"expiresAt": 1711013700000,
"type": "REGISTRATION"
}
- 오류 응답 (400 Bad Request - 잘못된 인증 코드)
{
"code": 2017,
"message": "INVALID_VERIFICATION_CODE",
"detail": "잘못된 인증 코드입니다.",
"metadata": {
"remainingAttempts": 9 // 실패 후 남은 시도 횟수
}
}
참고: 인증이 성공하면
verificationId가 발급됩니다. 이 ID는 회원가입 API 호출 시 필요합니다. 인증은expiresAt까지 유효합니다(일반적으로 인증 후 30분). 잘못된 코드 입력 시 최대 10회까지 시도 가능하며, 횟수 초과 시 일정 시간 동안 추가 시도가 제한됩니다.
5.1.5 회원가입 API
- HTTP 메서드: POST
- 경로: /auth/register
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
중요: 보안 강화를 위해 사용자 비밀번호는 반드시 클라이언트에서 해시 처리한 후 서버로 전송해야 합니다. 해시는 SHA-256 알고리즘을 사용하며, 솔트로 앱 인증 과정에서 받은
appSecret과 디바이스의 고유 ID(deviceUUID)를 함께 사용합니다. 해시된 비밀번호는passwordHash필드로 전송합니다.
요청 (Request)
{
"email": "user@example.com",
"passwordHash": "base64EncodedPasswordHashString",
"deviceId": "device_123",
"verificationId": "ver_456",
"accessCode": "ABCD1234EFGH5678",
"agreements": [
// 약관 동의 결과
{ "termsId": 1, "version": "1.0.0", "isAgreed": true }
],
"consents": {
// 선택적 동의 결과
"DATA_PROCESSING_IMPROVEMENT": false
},
"profile": {
"language": "de",
"timezone": "Europe/Berlin"
}
}
응답 (Response)
- 성공 응답 (201 Created): (기존과 동일)
{
"userId": "user_123",
"email": "user@example.com",
"profile": {
"language": "ko",
"timezone": "Asia/Seoul"
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"createdAt": 1711011600000
}
- 오류 응답 (400 Bad Request)
{
"code": 2013,
"message": "EMAIL_NOT_VERIFIED",
"detail": "이메일 인증이 완료되지 않았습니다."
}
- 오류 응답 (400 Bad Request)
{
"code": 1103,
"message": "REQUIRED_TERMS_NOT_AGREED",
"detail": "필수 약관에 동의하지 않았습니다."
}
- 오류 응답 (400 Bad Request)
{
"code": 3010,
"message": "ACCESS_CODE_NOT_VALIDATED",
"detail": "Access Code 검증이 완료되지 않았습니다."
}
- 오류 응답 (400 Bad Request)
{
"code": 2028,
"message": "REGISTRATION_VALIDATION_FAILED",
"detail": "가입 정보 검증에 실패했습니다."
}
- 오류 응답 (409 Conflict)
{
"code": 2020,
"message": "EMAIL_ALREADY_EXISTS",
"detail": "이미 사용 중인 이메일입니다."
}
5.2 로그인 및 인증 프로세스
📌 기술 구현 문서: 로그인 프로세스 구현 가이드 | 토큰 관리 명세
로그인 및 인증 프로세스는 다음과 같은 단계로 진행됩니다:
- 로그인: 사용자 인증 정보로 로그인하여 액세스 토큰과 리프레시 토큰을 발급받습니다.
- 토큰 관리: 액세스 토큰 만료 시 리프레시 토큰을 사용하여 새로운 토큰을 발급받습니다.
- 로그아웃: 토큰을 무효화합니다.
중요: 로그인 API 호출 시에도 앱 보안 인증을 통해 발급받은
appToken을Authorization헤더에 포함해야 합니다. 로그인 성공 후에는 사용자 인증 토큰(accessToken)이 발급되며, 이후 사용자 인증이 필요한 모든 API 호출은 이accessToken을 사용합니다. 토큰 갱신 및 검증 API 호출 시에도appToken을 사용해야 합니다.
5.2.1 로그인 API
- HTTP 메서드: POST
- 경로: /auth/login
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
중요: PIN 코드는 ISO27001, GDPR, DiGA 규제 준수를 위해 서버로 전송되지 않습니다. 모바일 앱에서는 사용자가 입력한 PIN 코드를 통해 로컬에 안전하게 저장된 인증 정보(ID/PW)를 복호화한 후, 이 인증 정보를 사용하여 로그인을 수행합니다. 이 방식은 사용자의 PIN 코드가 네트워크를 통해 전송되거나 서버에 저장되는 것을 방지하여 보안을 강화합니다.
참고: 모바일 앱 재설치 시에는 저장된 PIN 코드와 인증 정보가 손실됩니다. 이 경우 사용자는 이메일/비밀번호를 사용한 전체 로그인 과정을 다시 수행하고, 로그인 성공 후 새로운 PIN 코드를 설정해야 합니다. 상세한 재설치 시나리오 처리 방법은 인증 기술 명세서를 참조하세요.
보안 요구사항: 사용자 비밀번호는 반드시 해시된 형태로 서버에 전송되어야 합니다. 로그인 요청 시 일반 텍스트 비밀번호 대신
passwordHash필드를 사용하세요. 해시는 SHA-256 알고리즘과 솔트(appSecret + deviceUUID)를 함께 사용합니다.
요청 (Request)
{
"email": "user@example.com",
"passwordHash": "base64EncodedPasswordHashString",
"deviceId": "device_123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"tokenType": "Bearer",
"user": {
"userId": "user_123",
"email": "user@example.com",
"name": "John Doe"
}
}
5.2.2 토큰 갱신
- HTTP 메서드: POST
- 경로: /auth/token/refresh
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
참고: 디바이스 정보는 refreshToken JWT에서 자동으로 추출되므로 별도의 deviceId 파라미터가 필요하지 않습니다.
요청 (Request)
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
응답 (Response)
- 성공 응답 (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
5.2.3 토큰 검증
- HTTP 메서드: POST
- 경로: /auth/token/validate
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
응답 (Response)
- 성공 응답 (200 OK)
{
"valid": true,
"expiresIn": 1800
}
5.2.4 로그아웃
- HTTP 메서드: POST
- 경로: /auth/logout
- Headers:
- Authorization: Bearer
{accessToken}
- Authorization: Bearer
참고: 로그아웃 시 디바이스 정보는 JWT accessToken에서 자동으로 추출되므로 별도의 요청 파라미터가 필요하지 않습니다. 토큰 무효화는 JWT의
jti(토큰 ID)를 블랙리스트에 추가하는 방식으로 처리됩니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"message": "Successfully logged out"
}
5.3 동의 관리 프로세스
📌 참고: 동의 관리 프로세스에 대한 자세한 내용은 동의 도메인 엔드포인트 문서를 참조하세요.
경로 변경 안내: 동의 관리 API는 기존
/auth/consents에서/consents로 경로가 변경되었습니다.
등록 프로세스 중단
📌 구현 순서: 회원가입 프로세스가 구현된 후에 진행합니다.
등록 프로세스 중단 API는 진행 중인 사용자 등록 과정에서 발생하는 중단 상황을 처리하고, 관련 캐시 데이터들을 정리하여 재가입을 가능하게 합니다.
5.4.1 등록 프로세스 중단 API
- HTTP 메서드: DELETE
- 경로: /v1/auth/registration/abort
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}(필수)
- 비고: 진행 중인 사용자 등록 프로세스를 중단하고 관련 캐시 데이터들(이메일 인증 토큰, access-code 인증 상태, 등록 세션 등)을 정리합니다. 중단된 등록 프로세스로 인해 재가입이 불가능한 상황을 해결합니다. 이메일/accessCode 정보가 없는 경우 appToken의 deviceId를 사용하여 해당 디바이스와 연관된 모든 등록 캐시를 정리합니다.
요청 (Request) - Device ID 기반 등록 프로세스 중단 (기본 방식)
{
"reason": "앱 크래시 후 등록 프로세스 재시작"
}
참고: 요청 본문이 비어있거나 email/accessCode가 제공되지 않은 경우, appToken에서 추출한 deviceId를 사용하여 해당 디바이스와 연관된 모든 등록 관련 캐시를 정리합니다. 이는 앱 크래시, 강제 종료 등으로 클라이언트에서 이메일이나 accessCode 정보를 잃어버린 경우에 유용합니다.
요청 (Request) - 특정 이메일/Access Code 기반 (선택적)
{
"email": "user@example.com", // 또는 "accessCode": "ABCD1234EFGH5678"
"reason": "특정 이메일의 등록 프로세스만 중단"
}
참고: 특정 이메일이나 Access Code의 등록 프로세스만 중단하고 싶은 경우에만 사용합니다. 일반적으로는 위의 deviceId 기반 방식을 사용하는 것이 더 편리합니다.
응답 (Response)
- 성공 응답 (200 OK) - deviceId 기반 (기본 방식)
{
"message": "디바이스의 모든 등록 프로세스가 성공적으로 중단되었습니다.",
"deviceId": "device_123",
"clearedCaches": ["EMAIL_VERIFICATION_TOKEN", "ACCESS_CODE_VERIFICATION", "REGISTRATION_SESSION", "DEVICE_REGISTRATION_STATE"],
"abortedAt": 1714538000000
}
- 성공 응답 (200 OK) - 특정 이메일/accessCode 기반 (선택적)
{
"message": "등록 프로세스가 성공적으로 중단되었습니다.",
"email": "user@example.com", // 또는 "accessCode": "ABCD1234EFGH5678"
"clearedCaches": ["EMAIL_VERIFICATION_TOKEN", "ACCESS_CODE_VERIFICATION", "REGISTRATION_SESSION"],
"abortedAt": 1714538000000
}
- 오류 응답 (404 Not Found) - 진행 중인 등록 프로세스가 없는 경우
{
"code": 2071,
"message": "REGISTRATION_PROCESS_NOT_FOUND",
"detail": "해당 이메일, Access Code 또는 디바이스로 진행 중인 등록 프로세스를 찾을 수 없습니다."
}
- 오류 응답 (409 Conflict) - 이미 완료된 등록인 경우
{
"code": 2072,
"message": "REGISTRATION_ALREADY_COMPLETED",
"detail": "해당 이메일로 이미 등록이 완료된 사용자가 존재합니다."
}
- 오류 응답 (500 Internal Server Error) - 캐시 정리 실패
{
"code": 2073,
"message": "REGISTRATION_ABORT_FAILED",
"detail": "등록 프로세스 중단 중 오류가 발생했습니다."
}
계정 비활성화 (탈퇴)
📌 API 레퍼런스: 계정 비활성화 API
계정 비활성화 API는 현재 로그인한 사용자의 계정을 비활성화(Soft Delete)합니다. 일반 사용자와 게스트 사용자 모두 이 API를 통해 자발적으로 탈퇴할 수 있습니다.
5.5.1 계정 비활성화 API
- HTTP 메서드: DELETE
- 경로: /v1/auth/me
- Headers:
- Content-Type: application/json (선택)
- Authorization: Bearer
{accessToken}(필수)
- 비고: 현재 로그인한 사용자의 계정을 비활성화합니다. 게스트 계정의 경우 만료 타이머도 함께 해제됩니다.
요청 (Request)
{
"reason": "더 이상 서비스를 이용하지 않습니다."
}
참고: 요청 본문은 선택 사항입니다. 탈퇴 사유를 기록하고 싶은 경우에만 전송합니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"message": "계정이 성공적으로 비활성화되었습니다.",
"userId": "123e4567-e89b-12d3-a456-426614174000",
"status": "INACTIVE",
"deletedAt": 1702819200000
}
- 오류 응답 (400 Bad Request) - 이미 비활성화된 계정
{
"message": "이미 비활성화된 사용자입니다."
}
- 오류 응답 (401 Unauthorized) - 인증 실패
{
"code": 2051,
"message": "INVALID_TOKEN",
"detail": "유효한 사용자 인증 정보가 없습니다."
}
- 오류 응답 (404 Not Found) - 사용자를 찾을 수 없음
{
"message": "사용자를 찾을 수 없습니다."
}
Eid 인증 및 연동
📌 기술 구현 문서: Eid 연동 가이드 | Eid 명세
📌 구현 순서: 인증 핵심 기능 API가 구현된 후에 진행합니다.
아래 6.1~6.6 의 /auth/eid/* 경로는 초기 v1 설계 문서(레거시) 로, 현재 코드베이스에 대응 컨트롤러가 없습니다. EU 운영 계약은 v2 입니다 — eID 로그인/링크/언링크는 auth/login/eid/*(Plan 209 표준화), 가입은 auth/register/eid/*. 네임스페이스 표준은 auth/{register|login}/{provider}/*. 신규 구현·클라이언트는 v2 섹션을 따르세요.
Eid는 독일 의료 시스템에서 사용되는 디지털 건강 ID로, 사용자는 기존 DTA 계정과 Eid를 연동하여 추가적인 인증 방식으로 사용할 수 있습니다.
6.1 Eid 연동 초기화
- HTTP 메서드: POST
- 경로: /auth/eid/link/initialize
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{accessToken}
요청 (Request)
{
"redirectUri": "dta://eid-callback"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"authUrl": "https://eid.de/authorize?client_id=abc&state=xyz&redirect_uri=dta://eid-callback",
"state": "xyz123",
"expiresIn": 600
}
- 오류 응답 (401 Unauthorized)
{
"code": 2001,
"message": "UNAUTHORIZED",
"detail": "인증이 필요합니다."
}
6.2 Eid 연동 완료
- HTTP 메서드: POST
- 경로: /auth/eid/link/complete
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{accessToken}
요청 (Request)
{
"code": "auth_code_from_eid",
"state": "xyz123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"linked": true,
"eid": "g-id-12345",
"linkedAt": 1711011900000
}
- 오류 응답 (400 Bad Request)
{
"code": 2223,
"message": "EID_INVALID_CODE",
"detail": "Eid 인가 코드가 유효하지 않습니다"
}
- 오류 응답 (400 Bad Request)
{
"code": 2224,
"message": "EID_STATE_MISMATCH",
"detail": "Eid OAuth state가 일치하지 않습니다"
}
6.3 Eid 연동 상태 조회
- HTTP 메서드: GET
- 경로: /auth/eid/link
- Headers:
- Authorization: Bearer
{accessToken}
- Authorization: Bearer
응답 (Response)
- 성공 응답 (200 OK)
{
"linked": true,
"eid": "g-id-12345",
"linkedAt": 1711011900000,
"lastUsedAt": 1711016700000
}
- 성공 응답 (연동되지 않은 경우) (200 OK)
{
"linked": false
}
6.4 Eid 연동 해제
- HTTP 메서드: POST
- 경로: /auth/eid/unlink
- Headers:
- Authorization: Bearer
{accessToken}
- Authorization: Bearer
응답 (Response)
- 성공 응답 (200 OK)
{
"unlinked": true,
"unlinkedAt": 1711019400000
}
- 오류 응답 (400 Bad Request)
{
"code": 2220,
"message": "EID_NOT_LINKED",
"detail": "Eid가 연동되어 있지 않습니다"
}
6.5 Eid 로그인 초기화
- HTTP 메서드: POST
- 경로: /auth/eid/login/initialize
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"redirectUri": "dta://eid-login-callback",
"deviceId": "device_123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"authUrl": "https://eid.de/authorize?client_id=abc&state=xyz&redirect_uri=dta://eid-login-callback",
"state": "xyz123",
"expiresIn": 600
}
6.6 Eid 로그인 완료
- HTTP 메서드: POST
- 경로: /auth/eid/login/complete
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"code": "auth_code_from_eid",
"state": "xyz123",
"deviceId": "device_123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"tokenType": "Bearer",
"user": {
"userId": "user_123",
"email": "user@example.com",
"name": "홍길동"
}
}
- 오류 응답 (400 Bad Request)
{
"code": 2221,
"message": "EID_LOGIN_FAILED",
"detail": "Eid 로그인에 실패했습니다"
}
- 오류 응답 (404 Not Found)
{
"code": 2222,
"message": "EID_NOT_FOUND",
"detail": "Eid 연동 정보를 찾을 수 없습니다"
}
EU eID 회원가입
📌 참조 플랜: plan 177 — sleepq-de-eid-signup, plan 200(Access Code 가입 분리), plan 202(eID 로그인 게이트), plan 203(Native 2FA) 📌 대상: 신규 EU 환자.
cohort는 서비스 활성화에서 사용하는 AccessCode 발급 출처(issueSourceType)에서 파생됩니다 — 임상 대시보드 발급 =clinical, 그 외 =standard. 가입 시점에는 모두standard. 📌 데이터 거주성: 모든 트래픽·저장소는 europe-west3 (Frankfurt). GDPR/DiGA 준수.
v2 EU 인증 정책 (공통)
본 10·11·12 모든 endpoint 는 다음 정책을 공유합니다 — endpoint 별 알아둘 점에 다시 적지 않습니다.
- 인증 채널은
Authorization: Bearer한 줄: 토큰 종류(app token vs 사용자 access token)는 Authorization Bearer 의 payload 로 구분되고, FlexibleAuthGuard endpoint(login/eid/*·login/native/*)에서 두 종류를 모두 받아 분기합니다. 네임스페이스는auth/{register|login}/{provider}/*표준(Plan 209) — eID 로그인/링크/언링크는auth/login/eid/*, native 로그인은auth/login/native/*, 가입은 provider 무관auth/register/{provider}.- deviceId 는 token payload 에서: app token 발급 단계(
/v2/auth/app/*)만 body 로 deviceId(객체)+deviceIdHash 를 받습니다. 그 이후 모든 v2 EU endpoint 는request.appPayload.deviceId또는request.user.deviceId에서 가져옵니다. body 에deviceId를 보내면forbidNonWhitelisted로 400 VALIDATION_ERROR.- signup → 로그인 세션:
/v2/auth/register/{eid,native}/complete는 User+Profile(+EidLink / +디바이스 키) 생성 후login/{eid,native}와 동일한LoginV2ResponseDto(tokens + user + profile + roles + permissions + agreements) 를 반환합니다. 클라이언트는 access token 을 저장해 바로 (재)로그인하거나 그대로 사용하므로, 가입 직후 별도 first-login 을 반복하지 않습니다. 독립 first-login(12) 경로는 새 기기·키 분실 복구용으로 남습니다.- AppCheck 는 verify-only:
/v2/security/appcheck/verify가{verified, reason?}만 반환하며 세션토큰 발급 없음. 보호 endpoint 에 별도 AppCheck 헤더가 붙지 않습니다.
eID(gematik) 흐름을 신규 회원가입의 단일 진입점으로 사용합니다.
initialize는 OAuth state(10분 TTL)와 fbeta(gematik OIDC)로의 authorize URL 을 발급합니다. AccessCode 검증은 이후 가입에서 제거되어 service activation(POST /v2/auth/user-cycle/activate)으로 이동했습니다.complete는 fbeta 로부터 받은 code/state 와 약관·이메일 OTP 검증 ID 를 함께 받아User+EidLink를 생성하고 login/eid 와 동일한LoginV2ResponseDto세션을 반환합니다. 별도 first-login 을 반복하지 않습니다.
10.1 EU eID 회원가입 초기화
- HTTP 메서드: POST
- 경로:
/v2/auth/register/eid/initialize - Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"redirectUri": "dta://eid-signup-callback"
}
redirectUri만 받습니다.deviceId는 app token payload 에서 가져오므로 body 에 포함하면 400.accessCode는 에서 activation 으로 이동되어 가입 단계에서 받지 않습니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"authUrl": "https://eid-de-dev.weltcorp.com/auth/authorization?client_id=...&state=...&code_challenge=...&redirect_uri=...",
"state": "base64url-32B",
"expiresIn": 600,
"cohort": "standard"
}
- 응답에는
authUrl+state+expiresIn+cohort만 노출됩니다. PKCEcode_verifier/code_challenge와nonce는 서버가 Redis 에 자체 보관하며 클라이언트는state만 받아두면 됩니다 (complete단계에서 서버가 복원).
오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 1001 | VALIDATION_ERROR | redirectUri 누락/형식 오류, 또는 body 에 deviceId 같은 unknown 필드 포함 |
| 401 | 1000 | App token not provided / Missing deviceId claim in app token payload | app token 검증 실패 또는 payload 에 deviceId 누락 |
| 429 | 1000 | ThrottlerException: Too Many Requests | Endpoint throttle 5/분 |
10.2 EU eID 회원가입 완료 (ack)
- HTTP 메서드: POST
- 경로:
/v2/auth/register/eid/complete - Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"code": "auth_code_from_fbeta",
"state": "base64url-32B",
"email": "user@example.com",
"emailVerificationId": "verif_uuid_from_email_verify",
"agreements": [
{ "versionId": "agreement-v1", "isAgreed": true }
],
"profile": { "userName": "Erika Mustermann", "language": "de-DE" }
}
code/state는 클라이언트가authUrl로부터 redirect 콜백 (sleepqde://app/oauth2/callback?code=…&state=…)에서 회수한 값.state는 10.1 응답의state그대로.email+emailVerificationId는 3 이메일 OTP(type=REGISTRATION) 후 발급받은 검증 ID.deviceId/appCheckToken/platform은 body 에 받지 않습니다 — deviceId 는 app token payload, AppCheck 는 4 verify-only 호출로 검증 완료된 상태.
응답 (Response)
- 성공 응답 (201 Created) — 로그인 세션 (
LoginV2ResponseDto), login/eid 와 동일 형태
{
"tokens": [
{ "token": "<access-jwt>", "type": "ACCESS_TOKEN", "expiresIn": 3600, "issuedAt": 1714523700000 },
{ "token": "<refresh-jwt>", "type": "REFRESH_TOKEN", "expiresIn": 1209600, "issuedAt": 1714523700000 }
],
"user": { "id": "c1e7c5cf-...", "email": "user@example.com", "questionnaireBundleId": "default-bundle-id", "createdAt": 1714523700000 },
"profile": { "language": "de-DE", "timezone": { "id": "Europe/Berlin", "offsetInMinutes": 60 }, "userName": "Erika Mustermann" },
"roles": [],
"permissions": [],
"agreements": []
}
- 발급된 토큰은 REGISTERED 상태(아직 SERVICE_STARTED 아님) —
userCycle은 생략되고 access token payload 에uci가 없습니다.user-cycle/activate가uci가 박힌 토큰으로 재발급합니다.cohort/region/sessionPolicy는 응답 body 에 포함되지 않습니다(가입 시점 standard/EU 고정). - DB 영향: eID 연동 정보 저장 (status=ACTIVE) + 사용자 레코드 생성 (service_state=REGISTERED, region=EU, cohort=standard). userId 는 응답
LoginV2ResponseDto.user.id에서 바로 확인됩니다.
오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 1001 | VALIDATION_ERROR | body 필드 누락/형식 오류, 또는 deviceId·appCheckToken 같은 unknown 필드 포함 |
| 400 | 2008 | EMAIL_NOT_VERIFIED | email+emailVerificationId 가 REGISTRATION OTP 캐시와 불일치/만료 |
| 400 | 2058 | SIGNUP_KVNR_INVALID | fbeta 가 반환한 KVNR 이 [A-Z][0-9]{9} 형식이 아니거나 검증 실패 |
| 400 | 2223 | EID_INVALID_CODE | fbeta authorization code 무효, 토큰 교환 실패, 또는 이미 다른 사용자에 연동된 eID |
| 400 | 2224 | EID_STATE_MISMATCH | OAuth state 가 서버 캐시와 불일치/만료/소진 (replay 차단) |
| 401 | 1000 | App token not provided / Missing deviceId claim in app token payload | app token 검증 실패 또는 payload 에 deviceId 누락 |
| 409 | 2024 | DUPLICATE_EMAIL | 이미 가입된 이메일 |
| 412 | 2080 | REQUIRED_AGREEMENTS_NOT_AGREED | 139e / A_26591 필수 약관 미동의 |
| 429 | 1000 | ThrottlerException: Too Many Requests | Endpoint throttle 3/5분 |
EU Native 2FA 회원가입
📌 참조 플랜: plan 203 — sleepq-de-native-2fa 📌 대상: eID 대신 디바이스 키 기반 패스워드리스 가입을 사용하는 신규 EU 사용자. cohort 항상
standard.
10 의 v2 EU 인증 정책(Authorization Bearer 단일 채널 / token payload deviceId / signup → 로그인 세션 / AppCheck verify-only) 을 동일하게 따릅니다.
eID OAuth 대신 P-256 디바이스 키로 가입합니다. Plan 209 로 가입은 단일 POST /v2/auth/register/native 한 번 — 공개키(SPKI)만 등록하고 self-POP 서명·initialize(nonce) 단계는 제거됐습니다. 디바이스 키 소유 증명은 가입이 아니라 (재)로그인 서명에서 수행하므로, nonce TTL(5분) 만료로 인한 가입 실패 모드가 사라졌습니다.
11. EU Native 2FA 회원가입 (단일 endpoint)
- HTTP 메서드: POST
- 경로:
/v2/auth/register/native - Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"devicePublicKeySpki": "base64url-SPKI-DER-P256",
"email": "user@example.com",
"emailVerificationId": "verif_uuid_from_email_verify",
"agreements": [
{ "versionId": "agreement-v1", "isAgreed": true }
],
"profile": { "userName": "Erika Mustermann", "language": "de-DE" }
}
devicePublicKeySpki= 디바이스 공개키 SPKI DER 를 base64url 인코딩한 문자열(P-256 EC).signature는 받지 않습니다(Plan 209 — self-POP 제거). 보내면 unknown 필드로 400.email+emailVerificationId는 3 이메일 OTP(type=REGISTRATION) 후 발급받은 검증 ID — eID 가입과 동일 게이트.deviceId/appCheckToken/platform은 body 에 받지 않습니다 — deviceId 는 app token payload 에서.
응답 (Response)
- 성공 응답 (201 Created) — 로그인 세션 (
LoginV2ResponseDto), login/native 와 동일 형태
{
"tokens": [
{ "token": "<access-jwt>", "type": "ACCESS_TOKEN", "expiresIn": 3600, "issuedAt": 1714523700000 },
{ "token": "<refresh-jwt>", "type": "REFRESH_TOKEN", "expiresIn": 1209600, "issuedAt": 1714523700000 }
],
"user": { "id": "c1e7c5cf-...", "email": "user@example.com", "questionnaireBundleId": "default-bundle-id", "createdAt": 1714523700000 },
"profile": { "language": "de-DE", "timezone": { "id": "Europe/Berlin", "offsetInMinutes": 60 }, "userName": "Erika Mustermann" },
"roles": [],
"permissions": [],
"agreements": []
}
- 공개키 +
User+Profile+ACTIVE 디바이스 키 등록 후 로그인 세션을 반환합니다. REGISTERED 상태(userCycle생략,uci없음) —user-cycle/activate가uci박힌 토큰으로 재발급.cohort/region/sessionPolicy는 응답 body 에 없습니다. - DB 영향: 디바이스 키 등록 (status=ACTIVE) + 사용자 레코드 생성 (service_state=REGISTERED, region=EU, cohort=standard).
오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 1001 | VALIDATION_ERROR | body 필드 누락/형식 오류 또는 unknown 필드 포함 (signature 등 — Plan 209 로 제거됨) |
| 400 | 2008 | EMAIL_NOT_VERIFIED | email+emailVerificationId 가 REGISTRATION OTP 캐시와 불일치/만료 |
| 400 | 2215 | NATIVE2FA_PUBLIC_KEY_INVALID | devicePublicKeySpki SPKI 파싱 실패 또는 P-256 EC 키 아님 |
| 401 | 1000 | App token not provided / Missing deviceId claim in app token payload | app token 검증 실패 또는 payload 에 deviceId 누락 |
| 409 | 2024 | DUPLICATE_EMAIL | 이미 가입된 이메일 |
| 412 | 2080 | REQUIRED_AGREEMENTS_NOT_AGREED | 139e / A_26591 필수 약관 미동의 |
| 429 | 1000 | ThrottlerException: Too Many Requests | Endpoint throttle 3/5분 |
Plan 209 로 self-POP 가 제거되면서 가입 단계의
2213 NATIVE2FA_CHALLENGE_INVALID(nonce 만료/서명 실패)는 더 이상 발생하지 않습니다.
EU 로그인
📌 참조 플랜: plan 202(eID 로그인 게이트), plan 203(Native 2FA), signup → 로그인 세션 통합. 📌 대상: 활성 사용자의 재로그인, 그리고 새 기기·키 분실 시의 첫 로그인. (가입 직후 첫 로그인은 register 응답이 이미 세션을 주므로 불필요해졌고, 이 섹션 12.1 은 새 기기/복구 경로로 남습니다.)
10 의 v2 EU 인증 정책(Authorization Bearer 단일 채널 / token payload deviceId / AppCheck verify-only)을 동일하게 따릅니다.
STAGE 흐름 요약: ① signup(10·11) = User 생성 + 로그인 세션(LoginV2ResponseDto) 반환 (REGISTERED 상태) → ② user-cycle/activate = uci 가 박힌 새 토큰 재발급(SERVICE_STARTED) → ③ 재로그인/새 기기 첫 로그인(이 섹션 12.1·12.2·12.3·12.4). 별도 first-login 은 더 이상 가입 직후 필수가 아니며 새 기기·키 분실 복구용입니다.
eID 로그인 (/v2/auth/login/eid/*) 과 native 로그인 (/v2/auth/login/native/*) 은 서버 라우팅 가 Authorization Bearer 의 토큰 종류로 분기합니다 — user access token = 재로그인, app token = 첫 로그인 / 새 기기 첫 로그인 (body OTP 동반). Plan 209 로 native 의 app token 경로는 디바이스 키 서명이 아니라 email OTP + 공개키 upsert(keyless) 로 동작하며, 로그인 method 전환은 login/native/link·login/eid/link/* 로 일원화됐습니다.
12.1 첫 로그인 (새 기기·키 분실 복구 — REGISTERED → LoginV2ResponseDto)
가입(10·11)이 이미 로그인 세션을 반환하므로 가입 직후에는 이 단계가 필요 없습니다. 본 경로는 새 기기·키 분실 상황에서 access/refresh 를 재발급받는 데 씁니다(app token 을 그대로 Authorization: Bearer 에 싣고, eID 는 LOGIN OTP + fbeta OAuth 를, native 는 LOGIN OTP + 공개키 upsert(keyless, 서명 없음)를 인증 factor 로 사용).
12.1.1 eID 첫 로그인 시작 — POST /v2/auth/login/eid/initialize
- Headers:
Authorization: Bearer {appToken}(FlexibleAuthGuard, first-login = app token) - Body:
{ "redirectUri": "sleepqde://app/oauth2/callback" }
- 응답: 10.1 과 동일 형식(
authUrl/state/expiresIn/cohort).
12.1.2 eID 첫 로그인 완료 — POST /v2/auth/login/eid/complete
- Headers:
Authorization: Bearer {appToken}(FlexibleAuthGuard, first-login = app token) - Body:
{
"code": "auth_code_from_fbeta",
"state": "base64url-32B",
"email": "user@example.com",
"emailVerificationId": "verif_uuid_from_login_email_verify"
}
email+emailVerificationId는 3 이메일 OTP type=LOGIN 으로 새로 발급받은 검증 ID (REGISTRATION 과 별개 캐시).- 응답 (200 OK) —
LoginV2ResponseDto:
{
"user": { "id": "user_uuid", "email": "user@example.com" },
"userCycle": { "id": null, "status": null },
"tokens": [
{ "token": "eyJhbGciOi...", "type": "ACCESS_TOKEN", "expiresIn": 3600, "issuedAt": 1730000000 },
{ "token": "eyJhbGciOi...", "type": "REFRESH_TOKEN", "expiresIn": 1209600, "issuedAt": 1730000000 }
],
"roles": [],
"permissions": [],
"profile": { "userName": "Erika Mustermann", "language": "de-DE" },
"cohort": "standard",
"region": "EU",
"sessionPolicy": { "graceSeconds": 30, "idleTimeoutMinutes": 30, "activeTimeoutMinutes": 240 }
}
- 가입만 한 사용자이므로
userCycle.status는 아직 비어 있고 토큰 payload 에uci가 없습니다 —user-cycle/activate가 채웁니다.
12.1.3 native 첫 로그인 완료 (keyless) — POST /v2/auth/login/native/complete
Plan 209 — app token 경로는 keyless(서명 없음). initialize(nonce) 단계가 없고, complete 한 번으로 LOGIN OTP 검증 + 공개키 upsert 를 수행합니다.
- Headers:
Authorization: Bearer {appToken}(FlexibleAuthGuard, app token = keyless) - Body:
{
"email": "user@example.com",
"emailVerificationId": "verif_uuid_from_login_email_verify",
"devicePublicKeySpki": "base64url-SPKI-DER-P256"
}
email+emailVerificationId는 3 이메일 OTP type=LOGIN 검증 ID. 핸들러가 OTP 검증 후email로 userId 를 해소하고,devicePublicKeySpki를 ACTIVE 키로 upsert(expireAllForDevice+registerActive)합니다.- 이미 eID 링크된 사용자가 keyless 를 시도하면 409/2231(AUTH_METHOD_CONFLICT) — eID 로그인 후
login/native/link로 전환해야 합니다(exactly one auth method). - 응답: 12.1.2 와 동일한
LoginV2ResponseDto.
12.1 오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 1001 | VALIDATION_ERROR | body 필드 누락/형식 오류 또는 unknown 필드 포함 |
| 400 | 2008 | EMAIL_NOT_VERIFIED | (eID/native) LOGIN OTP 검증 ID 불일치/만료 |
| 400 | 2215 | NATIVE2FA_PUBLIC_KEY_INVALID | (native) keyless devicePublicKeySpki 파싱 실패 또는 P-256 EC 키 아님 |
| 400 | 2223 | EID_INVALID_CODE | (eID) fbeta code 무효, 토큰 교환 실패 |
| 400 | 2224 | EID_STATE_MISMATCH | (eID) OAuth state 불일치/만료 |
| 401 | 1000 | Authentication required / App token not provided | Authorization 검증 실패 (Authorization 누락 또는 무효 토큰) |
| 409 | 2231 | AUTH_METHOD_CONFLICT | (native) keyless 시도 사용자가 이미 eID 링크 보유 — eID 로그인 후 login/native/link 로 전환 |
| 429 | 1000 | ThrottlerException: Too Many Requests | Endpoint throttle (3 / 5분) |
12.2 동일 기기 재로그인
가입·활성화를 마친 활성(SERVICE_STARTED) 사용자가 같은 디바이스에서 eID 재로그인. Authorization: Bearer {userAccessToken} 으로 자기 access token 을 그대로 제시하면 서버는 user access token 분기로 라우팅, resolveReloginIdentity 가 userId 해석 + deviceId/eID 신원 일치를 검증합니다(타인 토큰 + 내 eID 차단).
POST /v2/auth/login/eid/initialize: body{ "redirectUri": "sleepqde://app/oauth2/callback" }+ Authorization=user access token.POST /v2/auth/login/eid/complete: body{ "code", "state" }만 —email/emailVerificationId불필요 (token 으로 신원 확정). 응답은 12.1.2 와 동일LoginV2ResponseDto(이 시점은userCycle.status = ACTIVE).
12.2 오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 2223 | EID_INVALID_CODE | fbeta code 무효 |
| 400 | 2224 | EID_STATE_MISMATCH | OAuth state 불일치/만료 |
| 401 | 1000 | Authentication required | Authorization 검증 실패 (Authorization 누락/무효/만료) |
| 404 | 2222 | EID_NOT_FOUND | 교환된 eID 해시에 매칭되는 활성 EidLink 없음 |
12.3 새 기기 첫 로그인
다른 디바이스에서 같은 eID 신원(동일 KVNR)으로 처음 로그인. 새 기기에는 user access token 이 없으므로 Authorization: Bearer {newAppToken} 으로 새 기기 app token 을 싣고 body 에 LOGIN OTP 를 동반합니다. FlexibleAuthGuard 가 request.appPayload 를 보고 first-login 분기로 라우팅, 핸들러가 (deviceId, email) 키의 OTP 검증 ID 와 대조합니다.
- 새 기기에서 1·2·3(type=LOGIN)·4 공통 게이트를 다시 수행한 뒤:
POST /v2/auth/login/eid/initialize: body{ "redirectUri" }, Authorization=app token.POST /v2/auth/login/eid/complete: body{ "code", "state", "email", "emailVerificationId" }, Authorization=app token. 응답은 12.1.2 와 동일.
12.3 오류 (Errors)
12.1·12.2 오류 표 합집합. 특히 OTP 누락 시 EMAIL_NOT_VERIFIED(2008), 매칭 eID 없으면 EID_NOT_FOUND(2222).
12.4 동일 기기 재로그인
디바이스 키를 가진 사용자가 같은 기기에서 재로그인. Authorization: Bearer {userAccessToken} 만 싣고(JwtAuthGuard 분기), userId·deviceId 는 token payload 에서 가져옵니다. 인증 자체는 디바이스 개인키로 server nonce 서명(body signature). initialize 는 body 없이 server nonce 만 받습니다.
POST /v2/auth/login/native/initialize: body 없음, Authorization=user access token.POST /v2/auth/login/native/complete: body{ "signature" }, Authorization=user access token. 응답은 12.1.2 와 동일.
12.4 오류 (Errors)
| HTTP | code | message | 비고 |
|---|---|---|---|
| 400 | 2213 | NATIVE2FA_CHALLENGE_INVALID | server nonce 만료 또는 서명 검증 실패 |
| 401 | 1000 | Authentication required | Authorization 검증 실패 |
| 404 | 2210 | NATIVE2FA_KEY_UNAVAILABLE | (userId, deviceId) 에 대해 ACTIVE 디바이스 키 없음 |
| 429 | 2214 | NATIVE2FA_ACCOUNT_LOCKED | 연속 실패로 디바이스 2FA 일시 잠금 |
12.5 로그인 method 전환 (link) — Plan 209
로그인된 사용자가 login method 를 전환하는 step-up 경로. 배타성 invariant("exactly one auth method") 를 지키며, 전환은 항상 email OTP step-up(또는 eID step-up) 을 거치고 반대편 method 를 같은 트랜잭션에서 만료합니다("requested method wins"). keyless 로그인(12.1.3)이 eID 사용자를 409 로 막는 것과 달리, link 는 step-up 을 거친 의도적 전환이므로 허용됩니다.
- eID → native:
POST /v2/auth/login/native/link(JwtAuthGuard + email OTP step-upemailVerificationId+ 새devicePublicKeySpki). 제출 키를 ACTIVE 로 등록하고 기존 eID 링크를 만료. 응답{ linked, method: "native", linkedAt, expiredEidLinks }. clinical cohort 는 403(native 불가). - native → eID:
POST /v2/auth/login/eid/link/initialize→ fbeta OAuth →POST /v2/auth/login/eid/link/complete(JwtAuthGuard). eID 링크를 ACTIVE 로 등록하고 native 디바이스 키를 만료. - eID 링크 상태/해제:
GET /v2/auth/login/eid/link(상태 조회),POST /v2/auth/login/eid/unlink(해제).
키 분실 복구(같은 method 유지)는 link 가 아니라 keyless 로그인(12.1.3)을 사용합니다 — 단, eID 사용자는 keyless 가 409 이므로 link 로만 전환합니다.
내부 운영자 인증 API
📌 기술 구현 문서: TBD
📌 구현 순서: 인증 핵심 기능 API가 구현된 후에 진행합니다.
내부 운영자 인증 API는 서비스 관리자, 의료진, 기술 담당자 등 내부 운영자를 위한 인증 서비스를 제공합니다. 이 API는 고객(환자) 인증과 유사하지만, 데이터는 operation 스키마에 저장되며 접근 권한이 더 제한적입니다.
7.1 내부 운영자 등록 API
- HTTP 메서드: POST
- 경로: /auth/operation/register
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{systemAdminToken}
중요: 내부 운영자는 System Admin 권한을 가진 사용자만 등록할 수 있습니다.
요청 (Request)
{
"email": "admin@example.com",
"passwordHash": "base64EncodedPasswordHashString",
"verificationId": "ver_456",
"userType": "OPERATION_USER",
"profile": {
"firstName": "관리자",
"lastName": "김",
"language": "ko",
"timezone": "Asia/Seoul"
},
"roles": ["SYSTEM_ADMIN"]
}
참고:
userType은 "OPERATION_USER" 또는 "SERVICE_ACCOUNT" 중 하나여야 합니다.roles는 IAM 도메인에서 정의된 역할 ID 목록입니다.
응답 (Response)
- 성공 응답 (201 Created)
{
"userId": "op_user_123",
"email": "admin@example.com",
"userType": "OPERATION_USER",
"profile": {
"firstName": "관리자",
"lastName": "김",
"language": "ko",
"timezone": "Asia/Seoul"
},
"roles": ["SYSTEM_ADMIN"],
"createdAt": 1711011600000
}
7.2 내부 운영자 삭제 API
- HTTP 메서드: DELETE
- 경로: /auth/operation/user/
{id} - Headers:
- Authorization: Bearer
{systemAdminToken}
- Authorization: Bearer
중요: 내부 운영자는 System Admin 권한을 가진 사용자만 삭제할 수 있습니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"message": "Operation user successfully deleted",
"userId": "op_user_123",
"deletedAt": 1711019400000
}
7.3 내부 운영자 이메일 인증 코드 발송 API
- HTTP 메서드: POST
- 경로: /auth/operation/email/verification-code
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{systemAdminToken 또는 appToken}
요청 (Request)
{
"email": "admin@example.com"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"email": "admin@example.com",
"expiresIn": 300,
"requestId": "req_op_123"
}
7.4 내부 운영자 이메일 인증 코드 확인 API
- HTTP 메서드: POST
- 경로: /auth/operation/email/verify
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{systemAdminToken 또는 appToken}
요청 (Request)
{
"email": "admin@example.com",
"code": "123456",
"requestId": "req_op_123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"verified": true,
"verificationId": "ver_op_456",
"verifiedAt": 1711011900000,
"expiresAt": 1711013700000
}
7.5 내부 운영자 로그인 API
- HTTP 메서드: POST
- 경로: /auth/operation/login
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"email": "admin@example.com",
"passwordHash": "base64EncodedPasswordHashString",
"deviceId": "device_123"
}
응답 (Response)
- 성공 응답 (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"tokenType": "Bearer",
"user": {
"userId": "op_user_123",
"email": "admin@example.com",
"userType": "OPERATION_USER",
"roles": ["SYSTEM_ADMIN"]
}
}
7.6 내부 운영자 로그아웃 API
- HTTP 메서드: POST
- 경로: /auth/operation/logout
- Headers:
- Authorization: Bearer
{operationUserToken}
- Authorization: Bearer
참고: 내부 운영자 로그아웃 시에도 디바이스 정보는 JWT 토큰에서 자동으로 추출되므로 별도의 요청 파라미터가 필요하지 않습니다.
응답 (Response)
- 성공 응답 (200 OK)
{
"message": "Successfully logged out"
}
7.7 내부 운영자 토큰 갱신 API
- HTTP 메서드: POST
- 경로: /auth/operation/token/refresh
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{operationUserToken}
참고: 내부 운영자 토큰 갱신 시에도 디바이스 정보는 refreshToken JWT에서 자동으로 추출되므로 별도의 deviceId 파라미터가 필요하지 않습니다.
요청 (Request)
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
응답 (Response)
- 성공 응답 (200 OK)
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 3600,
"tokenType": "Bearer"
}
7.8 내부 운영자 토큰 검증 API
- HTTP 메서드: POST
- 경로: /auth/operation/token/validate
- Headers:
- Content-Type: application/json
- Authorization: Bearer
{appToken}
요청 (Request)
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
응답 (Response)
- 성공 응답 (200 OK)
{
"valid": true,
"expiresIn": 1800,
"userType": "ADMIN",
"roles": ["SYSTEM_ADMIN"]
}
오류 코드
Auth 도메인 API에서 사용하는 오류 코드는 다음과 같습니다:
| HTTP 상태 코드 | 오류 코드 | 메시지 | 설명 | 대응 방법 |
|---|---|---|---|---|
| 500 | 2000 | SERVER_ERROR | 서버 내부 오류 | 서버 로그를 확인하고 시스템 관리자에게 문의하세요. |
| 401 | 2001 | UNAUTHORIZED | 인증이 필요합니다 | 유효한 토큰으로 재인증하세요. |
| 403 | 2002 | FORBIDDEN | 권한이 없습니다 | 필요한 권한을 확인하고 적절한 권한을 가진 계정으로 요청하세요. |
| 401 | 2003 | INVALID_APP_TOKEN | 유효하지 않은 토큰입니다 | 올바른 토큰으로 다시 요청하거나 재로그인하세요. |
| 401 | 2004 | APP_TOKEN_EXPIRED | 토큰이 만료되었습니다 | 리프레시 토큰을 사용하여 새로운 토큰을 발급받으세요. |
| 401 | 2005 | INVALID_REFRESH_TOKEN | 유효하지 않은 리프레시 토큰입니다 | 재로그인하여 새 리프레시 토큰을 발급받으세요. |
| 401 | 2006 | REFRESH_TOKEN_EXPIRED | 리프레시 토큰이 만료되었습니다 | 재로그인하여 새 토큰을 발급받으세요. |
| 401 | 2007 | INVALID_CREDENTIALS | 이메일 또는 비밀번호가 올바르지 않습니다 | 올바른 이메일과 비밀번호를 입력하세요. |
| 400 | 2007_1 | PASSWORD_HASH_INVALID | 비밀번호 해시가 유효하지 않습니다 | 올바른 해시 알고리즘과 솔트를 사용했는지 확인하세요. |
| 400 | 2007_2 | MISSING_PASSWORD_HASH | 해시된 비밀번호가 필요합니다 | 비밀번호를 해시하여 전송하세요. |
| 423 | 2008 | ACCOUNT_LOCKED | 계정이 잠겼습니다 | 고객센터에 문의하거나 비밀번호 재설정을 시도하세요. |
| 400 | 2009 | INVALID_DEVICE_ID | 디바이스 ID가 유효하지 않습니다 | 올바른 디바이스 정보로 다시 요청하세요. |
| 401 | 2010 | CHALLENGE_VERIFICATION_FAILED | 챌린지 검증에 실패했습니다 | 새로운 챌린지를 요청하고 앱 무결성을 확인하세요. |
| 403 | 2011 | DEVICE_BLACKLISTED | 디바이스가 블랙리스트에 등록되어 있습니다 | 계정 보안을 위해 고객센터에 문의하세요. |
| 400 | 2012 | INVALID_MFA_CODE | 유효하지 않은 2단계 인증 코드입니다 | 올바른 2단계 인증 코드를 입력하거나 새 코드를 요청하세요. |
| 400 | 2013 | EMAIL_NOT_VERIFIED | 이메일 인증이 완료되지 않았습니다 | 이메일 인증 과정을 완료하세요. |
| 400 | 2014 | VERIFICATION_CODE_EXPIRED | 인증 코드가 만료되었습니다 | 새 인증 코드를 요청하세요. |
| 401 | 2015 | INVALID_APP_TOKEN | 앱 토큰이 유효하지 않거나 만료되었습니다 | 앱 인증 과정을 다시 수행하세요. |
| 400 | 2026 | EMAIL_VERIFICATION_ALREADY_COMPLETED | 이미 인증이 완료된 이메일입니다 | 이미 인증된 이메일이므로 추가 인증이 필요하지 않습니다. |
| 400 | 2028 | REGISTRATION_VALIDATION_FAILED | 가입 정보 검증에 실패했습니다 | 입력한 정보가 유효한지 확인하고 필요한 정보를 모두 제공했는지 검토하세요. |
| 429 | 2040 | RATE_LIMIT_EXCEEDED | 요청 횟수가 제한을 초과했습니다 | 잠시 후에 다시 시도하세요. |
| 400 | 2223 | EID_INVALID_CODE | Eid 인가 코드가 유효하지 않습니다 | Eid 인증 과정을 다시 시작하세요. |
| 400 | 2224 | EID_STATE_MISMATCH | Eid OAuth state가 일치하지 않습니다 | 보안을 위해 인증 과정을 처음부터 다시 시작하세요. |
| 404 | 2220 | EID_NOT_LINKED | Eid가 연동되어 있지 않습니다 | Eid 연동 과정을 먼저 완료하세요. |
| 400 | 2221 | EID_LOGIN_FAILED | Eid 로그인에 실패했습니다 | Eid 계정 정보를 확인하고 다시 시도하세요. |
| 404 | 2222 | EID_NOT_FOUND | Eid 연동 정보를 찾을 수 없습니다 | Eid 연동 상태를 확인하고 필요시 재연동하세요. |
| 400 | 2057 | SIGNUP_STATE_MISMATCH | 가입 흐름의 상태가 유효하지 않거나 만료되었습니다 | EU eID 회원가입 흐름을 처음부터 다시 시작하세요. |
| 400 | 2058 | SIGNUP_KVNR_INVALID | KVNR 형식이 유효하지 않습니다 | 보험사에 KVNR(1자 + 9숫자) 발급 상태를 확인하세요. |
| 403 | 2060 | OPERATION_ACCESS_DENIED | 내부 운영자 접근 권한이 없습니다 | 내부 운영자 권한이 필요한 API입니다. |
| 400 | 2061 | INVALID_USER_TYPE | 유효하지 않은 사용자 유형입니다 | 유효한 운영자 유형을 입력하세요 (ADMIN, OPERATOR, CLINICIAN, SERVICE_ACCOUNT). |
| 400 | 2062 | INVALID_ROLE | 유효하지 않은 역할입니다 | 유효한 역할 ID를 입력하세요. |
| 404 | 2063 | OPERATION_USER_NOT_FOUND | 내부 운영자를 찾을 수 없습니다 | 내부 운영자 ID를 확인하세요. |
| 404 | 2071 | REGISTRATION_PROCESS_NOT_FOUND | 진행 중인 등록 프로세스를 찾을 수 없습니다 | 해당 이메일, Access Code 또는 디바이스로 진행 중인 등록 프로세스가 없습니다. |
| 409 | 2072 | REGISTRATION_ALREADY_COMPLETED | 이미 등록이 완료된 사용자가 존재합니다 | 해당 이메일로 이미 가입이 완료되어 등록 프로세스를 중단할 수 없습니다. |
| 500 | 2073 | REGISTRATION_ABORT_FAILED | 등록 프로세스 중단 처리 실패 | 등록 프로세스 중단 중 오류가 발생했습니다. 다시 시도하거나 고객센터에 문의하세요. |
| 401 | 2074 | IDENTITY_LEVEL_ALREADY_REGISTERED | 이미 정규 사용자로 승격된 계정입니다 | 일반 로그인 플로우를 이용하거나 기존 자격 증명을 확인하세요. |
| 401 | 2075 | DEVICE_MISMATCH_FOR_STEP_UP | Step-up 인증은 동일 디바이스에서만 수행할 수 있습니다 | 게스트 토큰을 발급받은 디바이스에서 다시 시도하세요. |
| 404 | 2076 | LINK_SESSION_NOT_FOUND | 유효한 Step-up 세션을 찾을 수 없습니다 | 세션이 만료되었거나 취소되었습니다. 새 Step-up 세션을 생성하세요. |
| 429 | 2077 | STEP_UP_TOO_MANY_ATTEMPTS | Step-up 인증 재시도 횟수를 초과했습니다 | 잠시 후 다시 시도하거나 고객센터에 문의하세요. |
| 400 | 3001 | INVALID_ACCESS_CODE | 유효하지 않은 Access Code입니다 | 올바른 Access Code를 입력하세요. |
| 400 | 3002 | ACCESS_CODE_ALREADY_USED | 이미 사용된 Access Code입니다 | 새로운 Access Code를 발급받아 사용하세요. |
| 400 | 3003 | ACCESS_CODE_EXPIRED | 만료된 Access Code입니다 | 새로운 Access Code를 발급받아 사용하세요. |
| 400 | 3010 | ACCESS_CODE_NOT_VALIDATED | Access Code 검증이 완료되지 않았습니다 | 회원가입 전에 Access Code 검증 과정을 완료하세요. |
| 404 | 1100 | TERMS_NOT_FOUND | 약관을 찾을 수 없습니다 | 약관 ID를 확인하고 다시 시도하세요. |
| 400 | 1101 | INVALID_TERMS_STATUS | 유효하지 않은 약관 상태입니다 | 약관 상태 값을 확인하고 올바른 값을 입력하세요. |
| 404 | 1102 | TERMS_TRANSLATION_NOT_FOUND | 약관 번역을 찾을 수 없습니다 | 언어 코드와 약관 ID를 확인하세요. |
| 400 | 1103 | REQUIRED_TERMS_NOT_AGREED | 필수 약관에 동의하지 않았습니다 | 필수 약관에 동의 후 다시 시도하세요. |
| 409 | 1104 | DUPLICATE_TERMS_TRANSLATION | 이미 존재하는 약관 번역입니다 | 다른 언어로 번역을 추가하거나 기존 번역을 업데이트하세요. |
| 400 | 1105 | ACTIVE_TERMS_ALREADY_EXISTS | 동일한 유형의 활성화된 약관이 이미 존재합니다 | 기존 활성 약관을 비활성화한 후 다시 시도하세요. |
| 404 | 1106 | TERMS_AGREEMENT_NOT_FOUND | 약관 동의 정보를 찾을 수 없습니다 | 사용자 ID와 약관 ID를 확인하세요. |
| 409 | 2020 | EMAIL_ALREADY_EXISTS | 이미 사용 중인 이메일입니다 | 다른 이메일 주소를 사용하거나 로그인을 시도하세요. |
| 404 | 2021 | EMAIL_NOT_FOUND | 존재하지 않는 이메일입니다 | 이메일 주소를 확인하거나 회원가입을 진행하세요. |
| 422 | 2027 | UNREGISTERED_EMAIL | 가입되지 않은 이메일입니다 | 이메일 주소를 확인하거나 회원가입을 진행하세요. |
변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-03-01 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-03-25 | bok@weltcorp.com | 초기 버전 작성 |
| 0.3.0 | 2025-04-11 | bok@weltcorp.com | 앱 인증 API 엔드포인트 이름 변경 (verify → complete-challenge, validate → verify-token) |
| 0.4.0 | 2025-04-11 | bok@weltcorp.com | 앱 토큰 검증 API 제거, 각 API에서 토큰 검증하도록 간소화 |
| 0.5.0 | 2025-04-11 | bok@weltcorp.com | 앱 토큰 조회 API 제거, 섹션 번호 조정 |
| 0.6.0 | 2025-04-30 | bok@weltcorp.com | 회원가입용과 비밀번호 재설정용 이메일 인증 코드 발송 API 분리 |
| 0.7.0 | 2025-05-07 | bok@weltcorp.com | 내부 운영자(operation user) 인증 관련 API 추가 |
| 0.8.0 | 2025-05-07 | bok@weltcorp.com | UNREGISTERED_EMAIL 오류 코드 추가 (422 HTTP 상태 코드) |
| 0.9.0 | 2025-05-08 | bok@weltcorp.com | 이메일 인증 API에 타입 필드 추가 (REGISTRATION, LOGIN, PASSWORD_RESET), /auth/terms-and-consents 경로를 /auth/agreements로 변경 |
| 0.9.1 | 2025-05-08 | bok@weltcorp.com | 별도 비밀번호 재설정용 이메일 인증 API 제거, 통합 이메일 인증 API로 일원화 |
| 0.9.2 | 2025-05-15 | bok@weltcorp.com | REGISTRATION_VALIDATION_FAILED (2028) 오류 코드 추가 |
| 0.10.0 | 2025-07-28 | bok@weltcorp.com | 등록 프로세스 중단 API 추가 (DELETE /v1/auth/registration/abort), deviceId 기반 캐시 정리 지원, 관련 오류 코드 추가 (2071-2073) |
| 0.11.0 | 2025-08-11 | bok@weltcorp.com | JWT stateless 인증 방식에 맞게 API 간소화: 로그아웃, 토큰 갱신 API에서 deviceId 파라미터 제거 (JWT에서 자동 추출) |
| 0.12.0 | 2025-10-27 | bok@weltcorp.com | 게스트 온보딩/Step-up API 추가 및 관련 오류 코드 확장 |