가맹점이 구현하고 Authly API 서버가 호출하는 표준 Bio API 스펙입니다.
이 API는 가맹점별 개별 adapter를 기본으로 하지 않고, Authly가 제공하는 표준 계약을 가맹점이 구현하는 방식입니다.
Base URL#
가맹점별로 Authly merchant 설정에 등록합니다. Base URL은 HTTPS만 등록할 수 있으며 credential, query, fragment를 포함할 수 없습니다.
https://merchant.example.com/authly/bio/v1
Authentication#
Authly API 서버는 가맹점 바이오 API 호출 시 Ed25519 request signature를 사용합니다.
Authly가 전역 서명 개인키로 요청에 서명하고, 가맹점은 Authly가 공개한 공개키로 서명을 검증해서 요청이 진짜 Authly API 서버에서 온 것인지 확인합니다. 가맹점은 OAuth token issuer를 구현하지 않으며, 검증용 비밀(secret)을 발급받거나 보관할 필요도 없습니다 — 공개키는 비밀이 아닙니다.
Headers#
X-Authly-Merchant-Id: merchant_123
X-Authly-Key-Id: authly-sign-2026-07
X-Authly-Timestamp: 2026-07-02T13:00:00Z
X-Authly-Nonce: 550e8400-e29b-41d4-a716-446655440000
X-Authly-Content-SHA256: hex_sha256_raw_body
X-Authly-Signature: v2=base64_ed25519_signature
X-Request-Id: req_...
Idempotency-Key: enr_01J... # write request only
Content-Type: application/json
X-Authly-Key-Id는 Authly 전역 서명키 id입니다 (가맹점별 값이 아님). 공개키 목록에서 이 id로 검증 키를 선택합니다.- signature header의 version prefix는
v2=입니다. 다른 prefix는 거부합니다.
Canonical Request#
서명 대상 문자열은 다음 순서와 newline을 고정합니다.
AUTHLY-ED25519
{http_method}
{path_and_query}
{timestamp}
{nonce}
{content_sha256}
Example:
AUTHLY-ED25519
POST
/authly/bio/v1/enrollments
2026-07-02T13:00:00Z
550e8400-e29b-41d4-a716-446655440000
8d969eef6ecad3c29a3a629280e686cff8fab8d8a39d7d...
Public Key Distribution#
Authly 서명 공개키는 무인증 공개 엔드포인트로 게시합니다.
GET {AUTHLY_API_BASE}/v1/signing-keys
{
"keys": [
{
"key_id": "authly-sign-2026-07",
"algorithm": "ed25519",
"public_key_base64": "MCowBQYDK2Vw...",
"status": "active"
}
]
}
public_key_base64는 SPKI DER를 base64 인코딩한 값입니다 (대부분의 언어 표준 라이브러리가 바로 읽는 형식).status는active(현재 서명에 사용) 또는retiring(rotation 중 병행 게시되는 구 키)입니다.- 가맹점은 요청의
X-Authly-Key-Id로 키를 선택합니다. key_id 기준으로 캐시하고, 모르는 key_id가 오면 목록을 재조회한 뒤 그래도 없으면 거부합니다. - 공개키는 비밀이 아니며, 위 엔드포인트의 응답을 기준으로 사용합니다.
Key Rotation#
서명키 교체는 공개키 병행 게시로 무중단 진행합니다.
- 정기 교체는 전환 시각을 최소 7일 전에 공지합니다. 공지에는 새
key_id, 전환 시각과 구 키 제거 시각을 KST와 UTC로 함께 명시합니다. - 전환 시 Authly가 새 키를
active, 기존 키를retiring으로/v1/signing-keys에 병행 게시하고 새 개인키로 서명을 시작합니다. - 가맹점은 key_id 기준 캐시 덕분에 추가 작업이 없습니다 (모르는 key_id 수신 시 목록 재조회만 구현되어 있으면 됨).
- 전환 후 7일 동안 기존 공개키를
retiring으로 병행 게시한 뒤 목록에서 제거합니다.
개인키 유출 또는 침해가 의심되는 긴급 교체는 예외입니다. 이 경우 사전 공지와 7일 병행 기간 없이 즉시 새 키로 전환하고, 유출이 의심되는 기존 공개키를 목록에서 제거한 뒤 가맹점에 긴급 공지합니다.
Verification Rules#
가맹점 바이오 API는 다음 순서로 검증합니다.
- 필수 header 존재 여부를 확인합니다.
X-Authly-Key-Id로 Authly 서명 공개키를 찾습니다 (/v1/signing-keys캐시).X-Authly-Merchant-Id가 가맹점 자신에게 발급된merchant_id와 일치하는지 확인합니다.- timestamp 허용 오차를 확인합니다. MVP 기본값은 5분입니다.
- nonce가 재사용되지 않았는지 확인합니다.
- raw request body bytes로
X-Authly-Content-SHA256을 다시 계산합니다. - canonical request를 구성합니다.
X-Authly-Signature의v2=뒤 base64 값을 Ed25519 서명 검증합니다.- 검증 성공 후에만 저장/조회 요청을 처리합니다.
Security Notes#
payload.features,payload.enc_key는 가맹점 DB에 저장되는 바이오 등록 데이터입니다.- 가맹점은 Bio API request/response body를 access log, application log, APM, tracing, error report에 남기기 금지
- signature mismatch 로그에도 request body 원문 남기기 금지
- nonce 저장소 장애 시 fail open 금지
- 서명 검증에 필요한 것은 공개키뿐이므로 가맹점 측에 유출 시 서명 위조로 이어지는 비밀이 없습니다. Authly 서명 개인키는 Authly만 보관합니다.
Common Error Response#
{
"error": {
"code": "signature_mismatch",
"message": "Request signature is invalid.",
"request_id": "req_..."
}
}
Error Codes#
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_request | 필수 field 누락 또는 형식 오류 |
| 401 | missing_signature | 서명 header 누락 |
| 401 | invalid_signature | signature 형식 오류 또는 알 수 없는 key_id |
| 401 | signature_mismatch | signature 검증 실패 |
| 401 | timestamp_out_of_range | timestamp 허용 오차 초과 |
| 409 | nonce_reused | nonce 재사용 |
| 409 | idempotency_conflict | 같은 idempotency key로 다른 body 요청 |
| 404 | bio_enrollment_not_found | 기존 등록 데이터 없음 |
| 400 | unsupported_finger | 지원하지 않는 finger code |
| 400 | unsupported_bio_type | 지원하지 않는 bio type |
| 500 | merchant_bio_internal_error | 가맹점 바이오 API 내부 오류 |
POST/enrollments#
등록 Flow에서 바이오 인증앱 callback으로 받은 바이오 등록 데이터를 가맹점 DB에 저장합니다.
재등록(overwrite) 정책: 같은 (merchant_user_ref + finger)로 다시 저장하면 기존 등록 데이터를 덮어씁니다(최신 등록이 유효). 재등록은 정상 시나리오(기기 변경, 품질 개선 등)입니다. 가맹점이 원하면 재등록 시 자체 추가 인증을 요구하는 것은 가맹점 재량입니다.
Method#
POST
Request#
{
"merchant_id": "merchant_123",
"merchant_user_ref": "user_abc",
"enrollment_session_id": "enr_01J...",
"type": "fingerprint",
"payload": {
"finger": "LI",
"features": "base64-or-provider-encoded-features",
"enc_key": "base64-or-provider-encoded-key"
},
"captured_at": "2026-07-02T13:03:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | yes | 가맹점 ID |
merchant_user_ref | string | yes | 가맹점 로그인 사용자 참조값. 개인정보 원문이나 단순 해시가 아닌 가맹점 내부 불투명 참조값 |
enrollment_session_id | string | yes | Authly 등록 세션 ID |
type | string | yes | MVP는 fingerprint. 인증 방식(payload 구조)을 구분하는 값이며, 향후 다른 방식이 추가되면 새 type 값과 그에 맞는 payload 구조가 추가됩니다 |
payload | object | yes | type에 따라 구조가 달라지는 등록 데이터. type: fingerprint의 구조는 아래와 같습니다 |
payload.finger | string | yes | 지원하는 finger code: LI, LM, RI, RM 중 하나 |
payload.features | string | yes | 바이오 인증앱이 생성한 바이오 등록 데이터 |
payload.enc_key | string | yes | 바이오 인증앱이 생성한 암호화 key |
captured_at | string | yes | Authly가 생성한 캡처 시각. 별도 캡처 시각이 없으면 callback 처리 시각 |
Response 200#
{
"stored": true,
"merchant_user_ref": "user_abc",
"type": "fingerprint",
"payload": { "finger": "LI" },
"enrollment_ref": "bio_enr_789",
"stored_at": "2026-07-02T13:03:02Z"
}
응답에서 stored: true는 필수입니다. merchant_user_ref, type, payload,
enrollment_ref, stored_at은 선택값입니다. 선택 필드를 반환하면 merchant_user_ref,
type은 요청값과 일치해야 하고, payload.finger를 반환하면 요청값과 일치해야 하며 지원
code여야 합니다. enrollment_ref는 빈 문자열일 수 없고 stored_at은 ISO 8601 timestamp여야
합니다. 불일치하거나 형식이 잘못된 응답은 세션 실패로 처리됩니다. payload.finger를 생략하면
Authly는 요청의 손가락 값을 사용합니다.
Idempotency#
Authly는 Idempotency-Key header에 enrollment_session_id를 보냅니다.
가맹점 바이오 API는 같은 Idempotency-Key와 같은 request body가 다시 들어오면 기존 저장 결과를 반환합니다. 같은 key로 다른 body가 들어오면 409 idempotency_conflict를 반환합니다.
Timeout and Retry#
Authly client 기준:
request timeout 기본값: 5s (연결 수립 포함, 요청 전체 기준)
retry 기본값: network error, timeout, 5xx에 한해 1회
4xx: retry 없음
timeout과 재시도 횟수는 가맹점별 설정값이며, 온보딩 시 다르게 설정할 수 있습니다.
Authly는 retry를 위해 payload.features, payload.enc_key를 DB나 durable queue에 저장하지 않습니다. retry는 현재 request memory 안에서만 수행합니다.
POST/enrollments/lookup#
확인 Flow에서 기존 등록 데이터를 조회합니다.
Method#
POST
조회 요청도 body를 사용합니다. merchant_user_ref를 URL path나 query에 넣지 않아 access log 노출 가능성을 줄입니다. 이 값 자체도 이메일, 전화번호, 이름, 주민등록번호 등 개인정보 원문이 아니어야 합니다.
Request#
{
"merchant_id": "merchant_123",
"merchant_user_ref": "user_abc",
"verification_session_id": "ver_01J...",
"type": "fingerprint",
"payload": {
"finger": "LI"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
merchant_id | string | yes | 가맹점 ID |
merchant_user_ref | string | yes | 가맹점 로그인 사용자 참조값. 개인정보 원문이나 단순 해시가 아닌 가맹점 내부 불투명 참조값 |
verification_session_id | string | yes | Authly 확인 세션 ID |
type | string | yes | MVP는 fingerprint. 인증 방식(payload 구조)을 구분하는 값이며, 향후 다른 방식이 추가되면 새 type 값과 그에 맞는 payload 구조가 추가됩니다 |
payload | object | yes | type에 따라 구조가 달라지는 조회 조건. type: fingerprint의 구조는 아래와 같습니다 |
payload.finger | string | yes | 조회할 finger code: LI, LM, RI, RM 중 하나 |
Response 200#
{
"found": true,
"merchant_user_ref": "user_abc",
"type": "fingerprint",
"payload": {
"finger": "LI",
"features": "base64-or-provider-encoded-features",
"enc_key": "base64-or-provider-encoded-key"
},
"enrollment_ref": "bio_enr_789",
"enrolled_at": "2026-07-02T12:30:00Z"
}
성공 응답에서 found: true, payload.features, payload.enc_key는 필수입니다. 나머지 필드는
선택값입니다. payload.features와 payload.enc_key는 빈 문자열일 수 없습니다. 선택 필드를
반환하면 merchant_user_ref, type은 요청값과 일치해야 하고, payload.finger를 반환하면
요청값과 일치해야 하며 지원 code여야 합니다. enrollment_ref는 빈 문자열일 수 없고
enrolled_at은 ISO 8601 timestamp여야 합니다. 불일치하거나 형식이 잘못된 응답은 세션 실패로
처리됩니다.
Response 404#
{
"error": {
"code": "bio_enrollment_not_found",
"message": "Bio enrollment was not found.",
"request_id": "req_..."
}
}
Security Notes#
- 응답의
payload.features,payload.enc_key는 Authly API 서버가 확인 처리 중에만 사용합니다. - Authly는 조회 응답의 바이오 payload를 DB, durable queue, 로그에 저장하지 않습니다.
- 가맹점은 필요한 사용자와 finger에 해당하는 최소 등록 데이터만 반환합니다.