공통 사전 단계 (v2)
v2 회원가입·로그인 흐름에서 본 endpoint(register/* · login/*) 호출 전에 거치는 공통 endpoint 마스터 spec. 4개 EU 인증 페이지(eID 회원가입·Native 2FA 회원가입·eID 로그인·Native 2FA 로그인)의 호출 순서 표는 본 페이지의 anchor 로 링크해서 참조한다.
| 분류 | 사용 시점 | endpoint |
|---|---|---|
| App Token 발급 | 새 디바이스 첫 호출(가입·새 기기 로그인) | A. /v2/auth/app/challenge + /complete-challenge |
| 약관 조회 | 회원가입 화면 (옵션, 로그인에서는 미사용) | B. GET /v2/mobile/app-settings |
| Email OTP | 회원가입(type=REGISTRATION), 로그인 시 새 기기 / 첫 로그인(type=LOGIN) | C. /v2/auth/email/verification-code + /verify |
| AppCheck 검증 | 회원가입·로그인 모든 흐름 | D. POST /v2/security/appcheck/verify |
| (외부) fbeta OAuth chain | eID 가입·로그인의 외부 IDP 단계 | E. fbeta FakeIDP OAuth chain |
- 인증 채널은
Authorization: Bearer한 줄. - deviceId 는 token payload 에서. 단 A 두 endpoint(토큰 발급 단계) 만 body 로 deviceId(객체)+deviceIdHash 를 받는다.
- AppCheck 는 verify-only. 보호 endpoint 에 별도 헤더가 붙지 않으며, 통과 여부는 verify 호출 결과로만 확인.
A. App Token 발급
새 디바이스에서 v2 인증을 시작하기 전 한 번 발급. 발급된 appToken 의 payload 에는 deviceIdHash 가 claim 으로 박혀 있어 이후 모든 v2 인증 endpoint 는 그 값을 deviceId 로 사용한다.
A-1. POST /v2/auth/app/challenge
🔗 라이브 명세 (Swagger UI): dev
-
인증: 공개 (가드 없음)
-
Request body:
{
"deviceId": {
"uuid": "iphone-15-uuid",
"platform": "ios",
"version": "1.0.0",
"timestamp": 1730000000000
},
"deviceIdHash": "sha256_hex_of_deviceId_json"
}- 이 두 endpoint(
/v2/auth/app/*) 만 body 로 deviceId(객체)+deviceIdHash 를 받는 예외 — 아직 토큰이 없어 payload 에서 가져올 수 없기 때문. deviceIdHash= SHA256(고정 순서·공백 없는 JSON:{"uuid":"..","platform":"..","version":"..","timestamp":<number>}). timestamp 는 number(따옴표 없음).
- 이 두 endpoint(
-
Response 200:
{
"challenge": "...",
"nonce": "..."
}
A-2. POST /v2/auth/app/complete-challenge
🔗 라이브 명세 (Swagger UI): dev
-
인증: 공개 (가드 없음)
-
Request body:
{
"deviceIdHash": "sha256_hex_of_deviceId_json",
"encryptedChallenge": "rsa_oaep_sha256_base64"
}encryptedChallenge= 서버 공개키로nonce + challenge문자열을 RSA-OAEP(SHA-256) 암호화 → base64.
-
Response 200:
{
"appToken": "eyJhbGciOi...",
"appSecret": "secureRandomString",
"expiresAt": 1730003600000,
"device": {
"id": "...",
"uuid": "...",
"platform": "ios",
"version": "1.0.0"
}
}- 이후 모든 v2 인증 endpoint 의
Authorization: Bearer <appToken>값.
- 이후 모든 v2 인증 endpoint 의
B. 약관 조회
GET /v2/mobile/app-settings
🔗 라이브 명세 (Swagger UI): dev
- 인증: 공개 (rate limit 만 적용)
- Required headers:
User-Agent: <product>/<ver> (<platform>; <os>; <device>; <channel>; locale/<xx-XX>)— 누락/형식 오류 시INVALID_USER_AGENT(8005).
- Response 200: 약관 목록(
agreements배열) + 기타 앱 설정. signup body 의agreements에는 응답에서{ versionId, isAgreed }만 추려 보낸다. - v1·v2 응답 동일.
회원가입에만 사용하며 로그인 흐름에서는 호출하지 않는다.
C. Email OTP
가입은 type=REGISTRATION, 로그인은 type=LOGIN 으로 호출. 같은 endpoint 지만 검증 캐시 네임스페이스가 다르므로 같은 type 으로 매치해야 한다.
C-1. POST /v2/auth/email/verification-code
🔗 라이브 명세 (Swagger UI): dev
-
인증: App Token (app token 검증)
-
Request body:
{
"email": "user@example.com",
"type": "REGISTRATION"
}type:REGISTRATION(가입) 또는LOGIN(로그인).
-
Response 200:
{
"success": true,
"email": "user@example.com",
"expiresIn": 300,
"requestId": "req_xxx",
"type": "REGISTRATION"
}
C-2. POST /v2/auth/email/verify
🔗 라이브 명세 (Swagger UI): dev
-
인증: App Token (app token 검증)
-
Request body:
{
"email": "user@example.com",
"code": "123456",
"requestId": "req_xxx",
"type": "REGISTRATION"
} -
Response 200 (
type=REGISTRATION):{
"valid": true,
"verificationId": "verif_xxx",
"verifiedAt": 1730000000000,
"expiresAt": 1730001800000,
"email": "user@example.com",
"type": "REGISTRATION"
} -
Response 200 (
type=LOGIN—lastLoginMethod포함):{
"valid": true,
"verificationId": "verif_xxx",
"verifiedAt": 1730000000000,
"expiresAt": 1730001800000,
"email": "user@example.com",
"type": "LOGIN",
"lastLoginMethod": "eid"
}- 발급된
verificationId는 후속 가입(register/eid/complete또는register/native/complete) 또는 로그인(eid/login/complete)의 bodyemailVerificationId로 그대로 전달. - 핸들러는
(deviceIdHash, email, type)키로 검증 ID 캐시를 조회하므로 같은 deviceId 의 같은 email + 같은 type 으로만 매치된다. lastLoginMethod(type=LOGIN에서만 반환): 해당 계정의 마지막 로그인 수단 = 현재 활성 인증 수단."eid"면 eID 로그인,"native"면 Native 2FA 로그인 으로 라우팅한다. EU 계정은 인증 수단을 정확히 1개만 가지므로(배타성 불변식) 활성 수단이 곧 마지막 로그인 수단이다. 활성 수단 미등록 등 조회 불가 시 필드 생략.
- 발급된
D. AppCheck verify
POST /v2/security/appcheck/verify
🔗 라이브 명세 (Swagger UI): dev
-
인증: App Token (app token 검증)
-
Request body:
{
"token": "<Firebase AppCheck token | Play Integrity token>",
"platform": "ios"
} -
Response 200 (성공):
{
"verified": true
} -
Response 200 (실패):
{
"verified": false,
"reason": "APPCHECK_PLAY_INTEGRITY_FAILED"
} -
응답은
{ verified, reason? }만 반환한다. 보호 endpoint 에 별도 AppCheck 헤더가 붙지 않으며 AppCheck 통과 여부는 verify 호출 결과로만 확인한다. 앱 부팅 후·로그인 시점에 한 번씩 호출해 attest 를 통과시키면 된다.
E. fbeta OAuth chain
eID register/eid/initialize 또는 eid/login/initialize 응답의 authUrl 을 외부 브라우저(ASWebAuthenticationSession / CustomTabsIntent) 로 열어 fbeta(gematik OIDC) 인증을 수행한다. 사용자가 eID 인증을 마치면 fbeta 는 deep link callback sleepqde://app/oauth2/callback?code=...&state=... 으로 돌려준다.
- 클라이언트는 callback URL 에서
code·state만 추출해 후속 complete body 로 전달한다 (PKCEcode_verifier와 fbeta 토큰 교환은 백엔드가 처리). - 자세한 chain 동작은 모바일 통합 가이드 참조.