본문으로 건너뛰기
버전: 개발 버전 (최신)

공통 사전 단계 (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 chaineID 가입·로그인의 외부 IDP 단계E. fbeta FakeIDP OAuth chain
인증 정책 (v2 공통)
  • 인증 채널은 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(따옴표 없음).
  • 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> 값.

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=LOGINlastLoginMethod 포함):

    {
    "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)의 body emailVerificationId 로 그대로 전달.
    • 핸들러는 (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 로 전달한다 (PKCE code_verifier 와 fbeta 토큰 교환은 백엔드가 처리).
  • 자세한 chain 동작은 모바일 통합 가이드 참조.

다음 단계