26 KiB
Directory API 외부 사용자 계정 생성 이용 가이드
이 문서는 외부 시스템이 Baron SSO의 Directory API를 사용해 외부 사용자 계정을 한 명씩 생성하는 방법을 설명합니다. API와 필드 이름은 OpenAPI 계약의 영문 표기를 그대로 사용합니다.
Staging Swagger UI: https://sso.hmac.kr/api/docs
Staging OpenAPI: https://sso.hmac.kr/api/openapi.yaml
Staging Swagger 화면
2026-08-26 기준으로 staging Swagger UI와 OpenAPI endpoint가 모두 HTTP 200을 반환하고, POST /api/v1/integrations/directory/users가 노출되는 것을 확인했습니다. 아래 화면은 해당 사용자 생성 항목을 펼친 실제 staging Swagger UI입니다.
Swagger UI의 Try it out은 실제 staging 요청을 전송합니다. 승인된 테스트 Tenant와 테스트용 Integration credential을 준비한 경우에만 사용하고, 실제 Secret이나 PII가 캡처·로그·issue에 남지 않도록 주의합니다.
1. 적용 범위
사용자 생성 API는 다음 endpoint입니다.
POST /api/v1/integrations/directory/users
이 endpoint는 외부 시스템용 기계 자격증명인 Integration credential로만 호출합니다. AdminFront 로그인 session이나 Authorization: Bearer ...를 Directory API 호출에 섞지 않습니다.
계정 생성 시 서버는 다음 identity 속성을 고정합니다.
| 속성 | 서버 고정값 | 호출자가 지정할 수 있는지 |
|---|---|---|
role |
user |
아니요 |
status |
active |
아니요 |
affiliationType |
external |
아니요 |
| 대표 Tenant | 요청의 primaryTenantId |
primaryTenantId로만 지정 |
role, status, affiliationType은 생성 응답에 모두 노출되는 필드가 아닙니다. 생성 후 관리자 조회로 최종 identity를 확인합니다.
근거: OpenAPI Directory 계약, Directory handler, Directory handler 테스트
2. 보안 경계와 사전 조건
호출 전에 다음 조건을 충족해야 합니다.
- 대상 Tenant의 UUID를 확인합니다.
primaryTenantId는 Tenant slug가 아닌 UUID여야 합니다. - 대상 Tenant를 포함하는
tenant_subtreescope의directory.user.createcredential을 발급합니다. - 대상 Tenant는 credential의 scope root 또는 그 descendant여야 합니다.
hanmac-family와 그 전체 subtree는 Directory 연동 대상이 아닙니다.- 요청마다 비어 있지 않은 새
Idempotency-Key를 준비합니다.
Integration credential은 interactive 관리자 권한이 아닙니다.
/api/v1/admin/*를 호출할 수 없습니다.- Admin session·Bearer token과 Integration credential header를 한 요청에 혼합할 수 없습니다.
- credential 하나에는 현재 정확히 하나의 OperationID만 부여합니다.
- Secret은 발급·교체·재발급 응답에서 한 번만 표시됩니다.
- 만료, revoke 또는 rotate로 무효화된 Secret은 인증에 사용할 수 없습니다.
- 요청 body의
email,name,phone은 Directory 감사 세부정보에 저장하지 않습니다. 호출 측에서도 Secret과 PII를 URL, application log, APM 또는 issue에 기록하지 마십시오.
근거: Integration credential domain, Integration credential 인증 middleware, Integration Credential 및 Directory API 정책
3. Integration credential 발급
3.1 AdminFront에서 발급
fresh identity의 raw role이 exact super_admin인 관리자가 다음 순서로 발급합니다.
- AdminFront의
/api-keys에서 API 키 관리를 엽니다. - Directory 연동 탭을 선택합니다.
- 이름과 발급 사유를 입력합니다.
- Operation에서 외부 사용자 생성(
directory.user.create)을 선택합니다. - Scope root Tenant를 선택합니다. Directory credential의 scope는 항상
tenant_subtree입니다. - 유효기간을
30,90,180,369일 또는만료 없음중에서 선택합니다. 기본값은 90일입니다. - 발급하고 Secret 확인을 선택한 뒤
clientId와clientSecret을 승인된 Secret 저장소에 즉시 저장합니다.
창을 닫으면 clientSecret을 다시 조회할 수 없습니다. 분실했을 때는 기존 Secret을 복구하지 말고 rotate하여 새 Secret으로 교체합니다.
근거: AdminFront API 키 관리 화면, AdminFront 관리 API client
3.2 관리 API에서 발급
관리 API를 이용할 때도 exact super_admin의 interactive access token이 필요합니다. 아래 값은 모두 예시 placeholder이며 실제 token, Secret 또는 운영 Tenant UUID를 문서나 shell history에 저장하지 마십시오.
BASE_URL='https://sso.hmac.kr'
ADMIN_ACCESS_TOKEN='<SUPER_ADMIN_ACCESS_TOKEN>'
SCOPE_TENANT_ID='11111111-1111-4111-8111-111111111111'
curl --fail-with-body --silent --show-error \
--request POST \
"${BASE_URL}/api/v1/admin/integration-credentials" \
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
--header 'Content-Type: application/json' \
--data "{
\"name\": \"Example HR directory user creator\",
\"operationIds\": [\"directory.user.create\"],
\"scope\": {
\"kind\": \"tenant_subtree\",
\"tenantId\": \"${SCOPE_TENANT_ID}\"
},
\"exposure\": \"structure\",
\"expiresInDays\": 90,
\"reason\": \"외부 사용자 계정 연동\"
}"
응답 예시는 다음과 같습니다. clientSecret은 이 응답에서만 한 번 표시됩니다.
{
"credential": {
"id": "22222222-2222-4222-8222-222222222222",
"name": "Example HR directory user creator",
"clientId": "bic_<EXAMPLE_KEY_ID>",
"operationIds": ["directory.user.create"],
"scopeKind": "tenant_subtree",
"scopeTenantId": "11111111-1111-4111-8111-111111111111",
"exposure": "structure",
"status": "active",
"creationReason": "외부 사용자 계정 연동",
"createdAt": "2026-08-24T00:00:00Z"
},
"clientSecret": "<DISPLAYED_ONCE_CLIENT_SECRET>"
}
Directory Operation에서는 exposure 입력값과 관계없이 서버가 structure로 고정합니다. global scope는 허용하지 않습니다. operationIds에는 정확히 하나의 OperationID만 넣어야 합니다.
관리 API에서 가능한 credential 작업은 다음과 같습니다.
| 목적 | Method 및 endpoint | 동작 |
|---|---|---|
| 목록 | GET /api/v1/admin/integration-credentials |
Secret을 제외한 발급 이력을 조회합니다. |
| 신규 발급 | POST /api/v1/admin/integration-credentials |
새 credential과 1회 표시 Secret을 발급합니다. |
| Secret 교체 | POST /api/v1/admin/integration-credentials/{id}/rotate |
기존 Secret을 즉시 무효화하고 새 Secret을 한 번 반환합니다. |
| 즉시 회수 | POST /api/v1/admin/integration-credentials/{id}/revoke |
credential을 revoked로 변경하고 Secret hash를 제거합니다. |
| legacy 재발급 | POST /api/v1/admin/integration-credentials/{id}/reissue |
inactive legacy 이력만 새 최소 권한과 새 Secret으로 재발급합니다. |
근거: OpenAPI credential 계약, Integration credential 관리 handler, server route
4. 사용자 생성 요청
4.1 Header
| Header | 필수 | 설명 |
|---|---|---|
Content-Type: application/json |
예 | JSON request body입니다. |
X-Baron-Key-ID |
예 | 발급 응답의 credential.clientId입니다. |
X-Baron-Key-Secret |
예 | 발급 응답에서 한 번 표시된 clientSecret입니다. |
Idempotency-Key |
예 | 호출자가 생성한 비어 있지 않은 요청 식별자입니다. UUID 사용을 권장합니다. |
4.2 허용 필드
요청 body는 strict JSON으로 처리합니다. 다음 필드만 허용됩니다.
| 필드 | 필수 | 형식 | 설명 |
|---|---|---|---|
email |
예 | 유효한 email address | 앞뒤 공백을 제거하고 lowercase로 정규화합니다. |
name |
예 | 비어 있지 않은 string | 앞뒤 공백을 제거합니다. |
primaryTenantId |
예 | UUID | credential scope 안에 있는 대표 Tenant입니다. |
phone |
아니요 | compact E.164 | +와 국가번호를 포함하며 공백·하이픈을 넣지 않습니다. 형식 예: +<COUNTRY_CODE><NATIONAL_NUMBER> |
4.3 금지 필드
허용 목록에 없는 필드는 400 invalid_request로 거부됩니다. 특히 다음 필드를 보내지 마십시오.
passwordrolestatusaffiliationTypetenantSlugmetadataadditionalTenantIdsadditionalAppointments- membership 또는 관계를 직접 표현하는 그 밖의 필드
첫 생성에서는 primaryTenantId 하나만 지정합니다. 추가 일반 소속은 사용자 생성 성공 후 별도의 directory.user.memberships.update credential과 membership PATCH로 처리합니다.
4.4 curl 예시
예시는 예약 domain인 example.test와 문서용 UUID만 사용합니다.
BASE_URL='https://sso.hmac.kr'
BARON_KEY_ID='bic_<EXAMPLE_KEY_ID>'
BARON_KEY_SECRET='<EXAMPLE_KEY_SECRET>'
PRIMARY_TENANT_ID='11111111-1111-4111-8111-111111111111'
IDEMPOTENCY_KEY='33333333-3333-4333-8333-333333333333'
curl --fail-with-body --silent --show-error \
--request POST \
"${BASE_URL}/api/v1/integrations/directory/users" \
--header 'Content-Type: application/json' \
--header "X-Baron-Key-ID: ${BARON_KEY_ID}" \
--header "X-Baron-Key-Secret: ${BARON_KEY_SECRET}" \
--header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
--data "{
\"email\": \"external.user@example.test\",
\"name\": \"Example External User\",
\"primaryTenantId\": \"${PRIMARY_TENANT_ID}\"
}"
위 예시는 PII로 오인될 수 있는 전화번호를 의도적으로 넣지 않았습니다. phone이 필요하면 compact E.164 값으로 추가하고, 필요하지 않으면 해당 key 전체를 생략합니다. 빈 문자열을 보내는 방식보다 필드를 생략하는 편이 호출 계약을 명확하게 유지합니다.
근거: OpenAPI DirectoryUserCreateRequest, strict JSON 및 입력 검증
5. 성공 응답과 idempotency
성공 시 HTTP 201 Created와 다음 형태의 응답을 반환합니다.
{
"userId": "44444444-4444-4444-8444-444444444444",
"email": "external.user@example.test",
"name": "Example External User",
"primaryTenantId": "11111111-1111-4111-8111-111111111111",
"status": "active"
}
userId는 이후 관리자 조회와 membership PATCH에 사용하는 Baron user UUID입니다. 응답에는 password가 포함되지 않습니다.
Idempotency-Key는 다음 원칙으로 관리합니다.
- 하나의 논리적 생성 요청에는 하나의 key를 사용합니다.
- timeout 또는 연결 종료로 결과를 받지 못했을 때만 같은 body와 같은 key로 재시도합니다.
- 같은 credential·같은 Operation·같은 key·같은 payload의 committed 요청은 기존 결과를 replay할 수 있습니다.
- 같은 key를 다른 email, name, phone 또는
primaryTenantId에 재사용하지 마십시오. 기존 mutation과 fingerprint가 다르면 idempotency conflict입니다. - 서버 내부 key namespace에는 credential ID와 OperationID가 포함되므로 서로 다른 credential 또는 Operation의 caller key가 우연히 같아도 같은 mutation으로 취급하지 않습니다.
- HTTP
5xx또는 network timeout 뒤 새 key로 즉시 다시 보내면 중복 생성 시도가 될 수 있습니다. 먼저 같은 key로 재시도하거나 관리자 조회로 결과를 확인합니다.
근거: Directory idempotency namespace, durable identity mutation 처리, identity mutation journal
6. 오류 처리
오류 body는 기본적으로 error, code, 선택적 details를 사용합니다.
{
"error": "primaryTenantId must be a UUID",
"code": "invalid_primary_tenant"
}
사용자 생성 경로에서 호출자가 주로 처리해야 하는 오류는 다음과 같습니다.
| HTTP | code |
의미와 조치 |
|---|---|---|
400 |
invalid_request |
지원하지 않는 필드, malformed JSON 또는 JSON 값이 두 개 이상입니다. body를 허용 필드만 포함하도록 수정합니다. |
400 |
idempotency_key_required |
Idempotency-Key가 없거나 비어 있습니다. 요청별 key를 지정합니다. |
400 |
invalid_email |
email 형식이 유효하지 않습니다. |
400 |
name_required |
trim 후 name이 비어 있습니다. |
400 |
primary_tenant_required |
primaryTenantId가 없습니다. |
400 |
invalid_primary_tenant |
primaryTenantId가 UUID가 아닙니다. |
400 |
invalid_phone |
phone이 compact E.164 형식이 아닙니다. |
401 |
integration_credential_required |
X-Baron-Key-ID 또는 X-Baron-Key-Secret이 없습니다. |
401 |
invalid_integration_credential |
key가 틀렸거나 inactive, revoked, rotate된 credential입니다. |
401 |
integration_credential_expired |
credential이 만료되었습니다. 새 credential을 발급하고 기존 credential의 revoke 여부를 운영 정책에 따라 결정합니다. rotate는 만료 시각을 연장하지 않습니다. |
401 |
credential_conflict |
interactive credential과 Integration credential을 혼합했습니다. Directory 요청에는 Integration credential header만 사용합니다. |
403 |
credential_operation_mismatch |
credential이 directory.user.create를 부여하지 않습니다. 생성 전용 credential을 사용합니다. |
403 |
credential_scope_exceeded |
대상 Tenant가 credential subtree 밖입니다. 응답 details.requiredScope를 확인하고 요청 또는 scope를 수정합니다. |
403 |
protected_tenant_scope |
hanmac-family 또는 그 subtree를 대상으로 요청했습니다. Directory 연동으로 처리할 수 없습니다. |
404 |
tenant_not_found |
primaryTenantId에 해당하는 Tenant가 없습니다. |
409 |
user_already_exists |
사용자가 이미 존재합니다. upsert하지 않으므로 기존 사용자와 후속 처리 정책을 확인합니다. |
409 |
idempotency_conflict |
같은 Idempotency-Key를 다른 mutation에 사용했습니다. 기존 요청 결과를 조사한 뒤 새 논리 요청에만 새 key를 사용합니다. |
503 |
credential_store_unavailable |
credential 저장소를 사용할 수 없습니다. mutation을 새 key로 반복하지 말고 같은 요청 key를 보존합니다. |
503 |
audit_unavailable |
mutation 전 필수 audit attempt를 저장할 수 없어 fail-closed했습니다. handler mutation은 실행되지 않습니다. |
503 |
credential_usage_record_failed |
감사 attempt 이후 credential 사용 시각을 안전하게 기록하지 못했습니다. handler mutation 전 실패입니다. |
503 |
directory_integration_unavailable |
Tenant 또는 identity write 의존성이 준비되지 않았습니다. |
503 |
tenant_graph_unavailable |
완전한 Tenant graph 또는 보호 root를 확인할 수 없어 fail-closed했습니다. |
503 |
directory_mutation_failed |
Directory mutation을 완료하지 못한 일반 오류입니다. 새 key로 반복하지 말고 같은 key를 유지해 재시도하거나 관리자에게 결과 확인을 요청합니다. |
idempotency_conflict는 handler가 의도한 오류 계약입니다. 다만 현재 저장소에는 같은 key와 다른 payload를 실제 HTTP로 호출해 409 mapping까지 확인하는 회귀 테스트가 없습니다. 호출자는 key를 재사용하지 않아야 하며, 이 경우 특정 code만 신뢰해 자동 복구하지 말고 모든 non-2xx 응답에서 처리를 중단한 뒤 기존 요청 결과를 확인해야 합니다.
credential_scope_exceeded에는 필요한 Operation과 concrete Tenant scope가 포함될 수 있습니다.
{
"error": "directory.user.create requires a credential covering the target tenant subtree",
"code": "credential_scope_exceeded",
"details": {
"requiredOperationId": "directory.user.create",
"requiredScope": {
"kind": "tenant_subtree",
"tenantId": "11111111-1111-4111-8111-111111111111"
}
}
}
근거: Error response schema, Directory 오류 분기, Integration credential 인증 오류
7. 생성 후 확인
201 응답의 userId, email, primaryTenantId, status를 우선 보관합니다. Integration credential에는 사용자 관리 조회 권한이 없으므로 최종 identity 확인은 권한이 있는 interactive 관리자 경로로 수행합니다.
BASE_URL='https://sso.hmac.kr'
ADMIN_ACCESS_TOKEN='<AUTHORIZED_ADMIN_ACCESS_TOKEN>'
USER_ID='44444444-4444-4444-8444-444444444444'
curl --fail-with-body --silent --show-error \
"${BASE_URL}/api/v1/admin/users/${USER_ID}" \
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}"
다음을 확인합니다.
- 응답
id가 생성 응답의userId와 같습니다. email과name이 정규화된 생성 값과 같습니다.role은user,status는active,affiliationType은external입니다.- 대표 Tenant가 요청한
primaryTenantId입니다.
identity commit 뒤 projection이 지연될 수 있으므로 관리자 readback이 즉시 보이지 않으면 제한된 간격과 횟수로 다시 조회합니다. 새 Idempotency-Key로 사용자 생성을 반복하지 않습니다.
근거: 관리자 user read route, 관리자 user 조회 handler, staging live acceptance
8. Password 설정
Directory 사용자 생성 요청과 응답에는 password가 없습니다. caller가 초기 password를 지정하거나 읽을 수 없으며, 서버 내부 identity 생성 절차에서 사용하는 password도 외부에 반환하지 않습니다.
사용자는 기존 password reset 흐름에서 본인의 login identifier로 재설정을 요청하고, 전달받은 일회성 link를 통해 새 password를 직접 설정합니다. email은 생성 필수값이므로 일반적으로 생성한 email을 login ID로 사용합니다.
POST /api/v1/auth/password/reset/initiate
Content-Type: application/json
{
"loginId": "external.user@example.test"
}
운영 시스템이 reset token 또는 새 password를 대신 수집해서는 안 됩니다. dryRun과 drySend는 운영에서 사용할 수 있는 일반 연동 옵션이 아닙니다.
근거: password reset route, password reset handler, password reset request model, password 미수신 테스트
9. 추가 소속은 별도 PATCH와 별도 credential 사용
사용자 생성용 directory.user.create credential로 membership PATCH를 호출하면 403 credential_operation_mismatch가 반환됩니다. 추가 일반 소속에는 directory.user.memberships.update 전용 credential을 별도로 발급합니다.
PATCH /api/v1/integrations/directory/users/{userId}/memberships
MEMBERSHIP_KEY_ID='bic_<MEMBERSHIP_KEY_ID>'
MEMBERSHIP_KEY_SECRET='<MEMBERSHIP_KEY_SECRET>'
USER_ID='44444444-4444-4444-8444-444444444444'
ADDITIONAL_TENANT_ID='55555555-5555-4555-8555-555555555555'
MEMBERSHIP_IDEMPOTENCY_KEY='66666666-6666-4666-8666-666666666666'
curl --fail-with-body --silent --show-error \
--request PATCH \
"${BASE_URL}/api/v1/integrations/directory/users/${USER_ID}/memberships" \
--header 'Content-Type: application/json' \
--header "X-Baron-Key-ID: ${MEMBERSHIP_KEY_ID}" \
--header "X-Baron-Key-Secret: ${MEMBERSHIP_KEY_SECRET}" \
--header "Idempotency-Key: ${MEMBERSHIP_IDEMPOTENCY_KEY}" \
--data "{
\"addTenantIds\": [\"${ADDITIONAL_TENANT_ID}\"],
\"removeTenantIds\": []
}"
PATCH는 대표 Tenant를 변경하지 않고 일반 소속 delta를 원자적으로 반영합니다. 현재와 결과 소속은 같은 최상위 Tenant root에 속해야 하며, 대표 Tenant 제거와 마지막 소속 제거는 허용하지 않습니다.
근거: OpenAPI membership 계약, membership handler, membership 회귀 테스트
10. 테스트 cleanup과 credential revoke
로컬 또는 staging 검증용으로 만든 resource는 검증이 끝난 뒤 생성의 역순으로 정리합니다.
- membership 검증을 완료합니다.
- 생성용과 membership용 credential을 각각 revoke해 추가 호출을 먼저 차단합니다.
- exact
super_admin또는 삭제 권한이 있는 interactive 관리자가 임시 사용자를 삭제합니다. - 테스트용 Tenant가 있다면 descendant부터 삭제합니다.
- 관리자 또는 phone-login 검증 session을 종료합니다.
임시 사용자 삭제 예시는 다음과 같습니다. 삭제도 별도의 Idempotency-Key를 사용합니다.
CLEANUP_IDEMPOTENCY_KEY='77777777-7777-4777-8777-777777777777'
curl --fail-with-body --silent --show-error \
--request DELETE \
"${BASE_URL}/api/v1/admin/users/${USER_ID}" \
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
--header "Idempotency-Key: ${CLEANUP_IDEMPOTENCY_KEY}"
credential revoke 예시는 다음과 같습니다.
CREDENTIAL_ID='22222222-2222-4222-8222-222222222222'
curl --fail-with-body --silent --show-error \
--request POST \
"${BASE_URL}/api/v1/admin/integration-credentials/${CREDENTIAL_ID}/revoke" \
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
--header 'Content-Type: application/json' \
--data '{"reason":"staging 사용자 생성 검증 종료"}'
실제 업무 계정은 테스트 cleanup 대상으로 삭제하지 않습니다. 운영 계정의 비활성화·삭제는 별도 lifecycle 및 승인 정책을 따릅니다. revoke된 credential의 이력은 남지만 해당 Secret은 다시 사용할 수 없습니다.
근거: credential revoke 구현, staging cleanup 구현
11. Staging 검증 사실
2026-08-24 staging live acceptance에서 connect phone-login으로 획득한 exact super_admin session을 사용해 다음 항목을 검증했습니다.
directory.user.create와directory.user.memberships.updatecredential을 분리 발급했습니다.- Directory 사용자 생성이
201로 성공하고 관리자 user readback의 ID가 일치했습니다. - 생성 전용 credential의 membership PATCH는
403 credential_operation_mismatch로 차단됐습니다. - membership 전용 credential로 같은 subtree의 descendant 소속 추가가
200으로 성공했습니다. - 생성 body의 금지 필드
additionalTenantIds는400 invalid_request로 거부됐습니다. - 검증 후 임시 사용자와 Tenant가 조회되지 않고 두 credential이
revoked상태이며 phone session도 제거된 것을 확인했습니다.
이 검증은 staging에서의 계약 검증 사실이며 production 배포 또는 production mutation을 의미하지 않습니다.
근거: staging live acceptance script, staging assertion, Integration Credential 및 Directory API 정책
12. 호출자 운영 체크리스트
directory.user.createexact Operation credential을 사용합니다.primaryTenantId가 credential의tenant_subtree안에 있습니다.- Secret과 PII를 log, URL, issue, Wiki 또는 source code에 남기지 않습니다.
- 요청 body에는 허용된 네 필드만 사용합니다.
- 논리 요청별
Idempotency-Key를 만들고 retry에서는 같은 key와 같은 body를 유지합니다. 201의userId를 보관하고 관리자 readback으로 고정 속성을 확인합니다.- password는 사용자 본인의 password reset 흐름으로 설정합니다.
- 추가 소속은 별도 membership credential과 PATCH로 처리합니다.
- 검증용 resource와 credential은 종료 시 cleanup 및 revoke합니다.
13. 1차 출처
- Backend OpenAPI
- Directory integration handler
- Directory integration handler 테스트
- Integration credential 관리 handler
- Integration credential 인증 middleware
- Integration credential domain
- Identity write service
- Identity mutation journal repository
- AdminFront API 키 관리 화면
- Staging Directory live acceptance
- Integration Credential 및 Directory API 정책
