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

Native 2FA 로그인 API (v2)

EU 사용자가 P-256 디바이스 키로 로그인하는 v2 endpoint. Authorization: Bearer 가 담는 토큰 종류로 두 경로가 갈린다. Plan 209 로 app token 경로는 디바이스 키 서명이 아니라 email OTP + 공개키 upsert(keyless) 로 동작한다 — 가입 직후·새 기기·분실 진입을 eID 의존 없이 단일 경로로 해결한다. 재로그인(user access token)은 기존 키 서명 그대로(강화). 로그인 method 전환은 별도 login/native/link 로 일원화됐다.

경로시점Authorization인증 factorbody 핵심
1. keyless 로그인 (가입 직후 / 새 기기 / 분실)가입 직후(REGISTERED), 또는 새 기기·키 분실app tokenemail OTP(type=LOGIN) + 공개키 upsertemail, emailVerificationId, devicePublicKeySpki
2. 동일 기기 재로그인서비스 활성화 후 같은 디바이스에서 재진입user access token디바이스 키 서명signature
3. method 전환 (link)이미 로그인된 사용자가 login method 를 native 로user access tokenemail OTP step-up + 새 공개키devicePublicKeySpki, emailVerificationId

서버는 complete endpoint 에서 Authorization Bearer payload 종류로 분기 — app token = keyless(공개키 upsert), user access token = 재로그인(서명).

인증 정책 (v2 공통)
  • 인증 채널은 Authorization: Bearer 한 줄.
  • deviceId·userId 는 token payload 에서.
  • keyless(app token): 디바이스 키 서명 없이 email OTP(type=LOGIN) 로 이메일 소유를 입증하고, body 의 devicePublicKeySpki 를 ACTIVE 키로 upsert(expireAllForDevice + registerActive)한다. 핸들러가 OTP 검증 후 email 로 userId 를 해소한다. 이미 eID 링크된 사용자가 keyless 를 시도하면 409/2231(AUTH_METHOD_CONFLICT) — eID 로 로그인한 뒤 3. link 로 전환해야 한다(exactly one auth method 불변).
  • 재로그인(user access token): token payload 로 신원이 확정되므로 OTP 불필요. server nonce 를 디바이스 비밀키로 서명한다.
  • AppCheck verify 자체는 app token 으로 호출(verify-only, 세션토큰 미발급).
  • 마스터 문서: domain endpoints.md (로그인).

1. keyless 로그인 (가입 직후 / 새 기기 / 분실)

Native 2FA 회원가입 은 이미 로그인 세션을 반환하므로 가입 직후에는 이 경로가 필수가 아니다. 본 keyless 경로는 주로 새 기기·키 분실 상황에서 access/refresh 를 재발급받는 데 쓴다. 디바이스 키 서명 없이 LOGIN OTP + 공개키 upsert 로 진입한다(eID 첫 로그인과 대칭). initialize(nonce) 단계가 없다 — app token 경로는 nonce 를 쓰지 않는다.

#APIAuthorizationbody 핵심응답에서 추출할 값
1POST /v2/auth/email/verification-codeBearer <appToken>{ "email", "type": "LOGIN" }requestId
2POST /v2/auth/email/verifyBearer <appToken>{ "email", "code", "requestId", "type": "LOGIN" }verificationId, lastLoginMethod
3POST /v2/security/appcheck/verifyBearer <appToken>{ "token", "platform" }{ verified: true }
4POST /v2/auth/login/native/completeBearer <appToken>{ "email", "emailVerificationId", "devicePublicKeySpki" }LoginV2ResponseDto

가입 직후 동일 기기면 가입 때 받은 app token 을 그대로 재사용한다. 새 기기POST /v2/auth/app/challenge·complete-challenge 로 새 app token 을 먼저 발급받은 뒤 ①~④ 를 거친다. ①② 의 type=LOGIN 은 가입 때 받은 REGISTRATION OTP 와 별개 캐시이므로 새로 발급받아야 한다. ④ complete 의 핸들러는 OTP(email/emailVerificationId) 를 검증한 뒤 email 로 userId 를 해소하고, devicePublicKeySpki 를 ACTIVE 키로 upsert 한다. eID 와 달리 fbeta OAuth chain 이 없어 외부 redirect 없이 끝난다.


2. 동일 기기 재로그인 (user access token)

활성(SERVICE_STARTED) 사용자가 같은 디바이스에서 재로그인. Authorization 에 자기 user access token 을 제시하면 서버는 user access token 분기로 라우팅하고, userId·deviceId 는 token payload 에서 가져온다. 인증 자체는 디바이스 비밀키로 server nonce 서명 (body signature). 재로그인은 initialize(nonce) → 서명 → complete 2단계다.

#APIAuthorizationbody 핵심응답에서 추출할 값
1POST /v2/security/appcheck/verifyBearer <appToken> (AppCheck 자체는 app token 필요){ "token", "platform" }{ verified: true }
2POST /v2/auth/login/native/initializeBearer <userAccessToken>없음 (body-less)challenge
3(클라이언트 내부) 디바이스 비밀키로 ②의 challenge 를 ES256 서명signature
4POST /v2/auth/login/native/completeBearer <userAccessToken>{ "signature" }LoginV2ResponseDto

클라이언트는 app token + user access token 둘 다 보관하고 있으며, AppCheck verify(①) 만 app token, 그 외 로그인 호출은 user access token.


이미 로그인된 사용자가 login method 를 native 로 전환하는 경로 (eID link 와 대칭). JwtAuthGuard(user access token) + email OTP step-up + 새 공개키를 받아 제출 키를 ACTIVE 로 등록하고, 기존 eID 링크를 같은 트랜잭션에서 만료한다(배타성 "requested method wins"). 세션은 이미 보유하므로 ack-only 응답. clinical cohort 는 native 2FA 불가 → 403.

#APIAuthorizationbody 핵심응답에서 추출할 값
1POST /v2/auth/email/verification-codeBearer <userAccessToken>{ "email", "type": "LOGIN" }requestId
2POST /v2/auth/email/verifyBearer <userAccessToken>{ "email", "code", "requestId", "type": "LOGIN" }verificationId, lastLoginMethod
3POST /v2/auth/login/native/linkBearer <userAccessToken>{ "devicePublicKeySpki", "emailVerificationId" }{ linked, method, linkedAt, expiredEidLinks }

이 경로는 eID → native 로의 method 전환 전용이다. 키 분실 복구(같은 method 유지)는 1. keyless 로그인 을 쓴다 (단, eID 사용자는 keyless 가 409 이므로 link 로 전환).


본 endpoint Spec

LI. POST /v2/auth/login/native/initialize

재로그인(2.) 전용 — server nonce 를 발급한다. keyless(app token) 경로는 이 endpoint 를 호출하지 않는다(nonce 불요).

  • Path: POST /v2/auth/login/native/initialize
  • 인증: 서버 라우팅 — 재로그인은 Bearer <userAccessToken>
  • Rate Limit: 5/분
  • 🔗 라이브 명세 (Swagger UI): dev

Request

HeaderValue
AuthorizationBearer <userAccessToken>
Content-Typeapplication/json

body 없음. deviceId·userId 는 token payload 에서. body 를 보내면 ValidationPipe 가 unknown value 로 거부한다.

Response 200 OK

{
"challenge": "base64url-32B-server-nonce",
"expiresIn": 300
}
필드설명
challenge디바이스 개인키로 ES256(P-256, SHA-256, IEEE-P1363) 서명해야 할 서버 nonce (32바이트 base64url).
expiresInnonce TTL (초, 기본 300). 시한 내 complete 를 호출해야 한다.

Errors

HTTPcodemessage발생 조건
4011000Authentication requiredAuthorization 검증 실패 (Authorization 누락, 무효 토큰, 만료)
4011000Missing deviceId claim in access token payloaduser access token payload 에 deviceId 누락
4042210NATIVE2FA_KEY_UNAVAILABLE(userId, deviceId) 에 대응하는 ACTIVE 디바이스 키 없음
4291000ThrottlerException: Too Many RequestsEndpoint throttle 5/분

LC. POST /v2/auth/login/native/complete

  1. keyless·2. 재로그인 — 두 경로 모두 같은 endpoint. Authorization 토큰 종류로 body 가 갈린다.
  • Path: POST /v2/auth/login/native/complete
  • 인증: 서버 라우팅 (FlexibleAuthGuard — app token 또는 user access token)
  • Rate Limit: 5/분 (prod 상시 적용; dev/stage/local은 THROTTLE_DISABLED=true 설정 시에만 해제)
  • 🔗 라이브 명세 (Swagger UI): dev

Request

HeaderValue
Authorization1. = Bearer <appToken>, 2. = Bearer <userAccessToken>
Content-Typeapplication/json

같은 endpoint 이지만 Authorization 토큰 종류에 따라 body 가 완전히 달라진다. keyless(1.)는 서명을 보내지 않고 OTP+공개키를, 재로그인(2.)은 서명만 보낸다.

필드타입1. app token (keyless)2. user access token (재로그인)설명
signaturestring보내지 않음필수LI 응답 challenge 를 디바이스 비밀키로 ES256 서명한 값 (IEEE-P1363, 64B → base64url).
emailstring필수보내지 않음사용자 이메일 (type=LOGIN OTP 로 검증한 값). userId 해소 키.
emailVerificationIdstring필수보내지 않음type=LOGIN OTP verify 가 반환한 verificationId.
devicePublicKeySpkistring필수보내지 않음upsert 할 디바이스 공개키 SPKI DER base64url (P-256). OTP 검증 후 ACTIVE 로 등록.

keyless(1.)는 app token 만으로는 사용자를 단정할 수 없어 LOGIN OTP 로 이메일 소유를 입증하고 email 로 userId 를 해소한 뒤 devicePublicKeySpki 를 upsert 한다(서명 없음). 재로그인(2.)은 token payload 의 userId/deviceId 로 신원이 확정되므로 OTP·공개키 동반이 불필요하고 server nonce 서명만 검증한다.

Body 예시 — 1. keyless (app token 분기)

{
"email": "user@example.com",
"emailVerificationId": "verif_uuid_from_login_email_verify",
"devicePublicKeySpki": "MFkwEwYHKoZIzj0CAQ...public-key-spki-base64url"
}

Body 예시 — 2. 재로그인 (user access token 분기)

{
"signature": "MEUCIQD...device-key-signature-base64url"
}
body 에 받지 않는 필드

deviceId, userId 는 body 에 받지 않는다.

Response 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"
},
"agreements": []
}

cohort/region/sessionPolicy 는 응답 body 에 포함되지 않는다 — 모든 EU 인증 응답(register·login, eID·native)은 LoginV2ResponseDto(v1 LoginResponseDto와 동일 형태) 로 통일돼 있다.

    1. (keyless, 가입 직후): userCycle.status = null, access token payload 에 uci 없음 (REGISTERED). 다음 단계 POST /v2/auth/user-cycle/activateuci 가 박힌 새 토큰 재발급.
    1. (재로그인): userCycle.status = "ACTIVE", access token payload 에 uci 포함.

Errors

HTTPcodemessage발생 조건
4001001VALIDATION_ERRORbody 필드 길이·형식 오류 또는 unknown 필드 포함
4002008EMAIL_NOT_VERIFIED1. keyless 에서 LOGIN OTP email/emailVerificationId 누락·불일치·만료. 재로그인(2.)에서는 발생하지 않음
4002213NATIVE2FA_CHALLENGE_INVALID2. 재로그인에서 server nonce 누락/만료 또는 서명 검증 실패
4002215NATIVE2FA_PUBLIC_KEY_INVALID1. keyless devicePublicKeySpki SPKI 파싱 실패 또는 P-256 EC 키 아님
4011000Authentication requiredAuthorization 검증 실패
4042210NATIVE2FA_KEY_UNAVAILABLE2. 재로그인에서 (userId, deviceId) 에 대응하는 ACTIVE 디바이스 키 없음
4092231AUTH_METHOD_CONFLICT1. keyless 시도 사용자가 이미 eID 링크 보유 — eID 로그인 후 link 로 전환
4292214NATIVE2FA_ACCOUNT_LOCKED연속 서명 실패로 디바이스 2FA 일시 잠금 (약 30분, BSI O.Auth_7)
4291000ThrottlerException: Too Many RequestsEndpoint throttle 5/분 (prod 상시 적용)

LK. POST /v2/auth/login/native/link

이미 로그인된 사용자가 login method 를 native 로 전환한다 (eID link 와 대칭).

  • Path: POST /v2/auth/login/native/link
  • 인증: JwtAuthGuard (user access token 전용)
  • Rate Limit: 5/시간 (factor 변경은 강한 인증 후에만 — BSI O.Auth_7/11/14)
  • 🔗 라이브 명세 (Swagger UI): dev

Request

HeaderValue
AuthorizationBearer <userAccessToken>
Content-Typeapplication/json
{
"devicePublicKeySpki": "MFkwEwYHKoZIzj0CAQ...public-key-spki-base64url",
"emailVerificationId": "verif_uuid_from_login_email_verify"
}
필드타입필수설명
devicePublicKeySpkistringYesACTIVE 로 등록할 새 디바이스 공개키 SPKI DER base64url (P-256).
emailVerificationIdstringYestype=LOGIN OTP verify 가 반환한 verificationId — email OTP step-up(2FA factor 변경) 증명.

Response 201 Created — ack-only

{
"linked": true,
"method": "native",
"linkedAt": 1733650000000,
"expiredEidLinks": 1
}
필드설명
linked항상 true.
method전환된 method — "native".
linkedAt전환 시각 (epoch ms).
expiredEidLinks이 전환으로 만료된 eID 링크 수 (없으면 0).

Errors

HTTPcodemessage발생 조건
4001001VALIDATION_ERRORdevicePublicKeySpki/emailVerificationId 형식·길이 위반
4002215NATIVE2FA_PUBLIC_KEY_INVALID제출 SPKI 파싱 실패 또는 P-256 EC 키 아님
4011000Authentication required / EMAIL_NOT_VERIFIEDJWT 검증 실패, deviceId claim 부재, 또는 email OTP step-up 미검증
4031000Native 2FA is not available for the clinical cohortclinical cohort 는 native 2FA 불가 (eID 전용)
4291000ThrottlerException: Too Many RequestsEndpoint throttle 5/시간

다음 단계

  • 1. keyless 직후서비스 활성화: POST /v2/auth/user-cycle/activate.
  • 2. 재로그인 후 → 일반 보호 endpoint 호출 (user access token 으로).
  • 3. link 후 → 이후 로그인은 native(디바이스 키) 경로로.
  • eID 로그인: eID 로그인.
  • Native 2FA 가입: Native 2FA 회원가입.
  • 마스터 문서: domain endpoints.md (로그인).