diff --git a/docs/sso-tenant-organization-review.md b/docs/sso-tenant-organization-review.md new file mode 100644 index 0000000..ea37e41 --- /dev/null +++ b/docs/sso-tenant-organization-review.md @@ -0,0 +1,125 @@ +# SSO 사용자 테넌트와 회사·부서 매핑 확인 + +확인일: 2026-10-30 + +## 확인 결과 + +사용자 소속 목록과 조직 계층을 조회하는 경로가 있다. 다만 로그인 claim, 본인 가입 테넌트, Integration Credential로 조회하는 조직 subtree는 서로 다른 범위다. `tenants` scope를 요청했다고 사용자의 모든 소속과 모든 상위 조직이 자동으로 전달된다고 가정하면 안 된다. + +요청자가 확인한 표시 기준은 다음과 같다. 이 이름은 기대 결과이며 실제 SSO 응답과 UUID를 조회해 검증한 결과는 아니다. + +| 항목 | 기대 값 | +| --- | --- | +| 회사 | 총괄기획실&기술개발센터 | +| 부서 | IS3 | + +사용자 추가 확인: `STAGING_ORG_CONTEXT_TENANT_IDS`에는 회사나 IS3가 아닌 **한맥가족 등 최상위 분류 조직**의 UUID를 등록했다. UUID 원문과 실제 조직 응답은 아직 확인하지 않았다. + +### 실제 세션 응답으로 확인된 상태 + +사용자가 전달한 로그인 후 `/auth/session` 응답의 관련 필드는 다음과 같다. 개인 연락처와 사용자 UUID는 이 문서에 복사하지 않는다. + +```json +{ + "authenticated": true, + "user": { + "tenantName": "IS3", + "company": "IS3", + "companySource": "sso", + "department": "", + "orgContextStatus": "unavailable" + } +} +``` + +이 응답으로 **현재 사용자 조직 조회·응답 처리에 실패하여 기존 SSO 값이 유지되고 있음**은 확인됐다. 조직 조회에 성공한 뒤 회사 노드를 잘못 선택했다고 확정할 수 있는 상태는 아니다. 코드상 `unavailable`은 구성 검사, HTTP 호출, 응답 해석·계층 검증 등의 예외를 함께 나타낸다. 사용자 UUID를 찾지 못한 경우의 `not_found`, 복수 소속을 선택하지 못한 경우의 `ambiguous_membership`과도 다르다. + +[최근 스테이징 배포 로그](https://gitea.hmac.kr/disong/baron_qa_write/actions/runs/223175/jobs/247461)에서는 `org:verify`가 `rootCount=7`, `tenantCount=560`, `userCount=4607`, `membershipCount=5370`으로 성공했다. 배포 당시 CI에서 키·조회 범위·응답 처리 전체가 성공했다는 증거이며 현재 Worker에서도 성공한다는 증거는 아니다. + +CI 검증은 요청당 15초, 실제 Worker 조회는 요청당 5초 제한이다. Worker의 `org_context_timeout`, `org_context_http_401`, `org_context_http_403`, `org_context_request_failed` 등 정확한 로그 코드를 확인해야 통신·시간 제한·runtime credential 문제를 구분할 수 있다. 세션 응답에 있는 빈 `scope`나 중복 `tenantIds`만으로 조직 API 실패 원인을 단정하지 않는다. + +사용자가 추가로 전달한 로그는 `/auth/session`의 HTTP 200 요청 기록이며 `outcome=ok`, `wall_time_ms=3`이다. 코드가 조직 예외를 처리한 뒤 정상 HTTP 응답을 반환하므로 이 요청 기록에는 내부 오류가 직접 표시되지 않을 수 있다. 이 시간만으로 타임아웃을 확정할 수 없다. + +로그 조회만으로 원인을 구분하지 못한 문제를 보완하기 위해 `user.orgContextError`에 허용된 오류 코드만 추가했다. 임의의 upstream 메시지는 숨기고, 성공 시 이전 오류 값은 제거한다. 배포 후 `/auth/session`의 이 필드로 원인을 확인할 수 있으며 실제 사용자 조직 조회 실패 자체를 해결한 변경은 아니다. + +## 조회 경로 + +| 경로 | 확인한 필드·범위 | Q&A 적용 시 주의점 | +| --- | --- | --- | +| OIDC `/oidc/userinfo` 및 ID token | RP scope·claim 정의와 동의에 따라 전달되는 사용자 claim | 회사·부서·상위 계층과 전체 소속이 항상 있다는 계약은 가이드에서 확인되지 않음 | +| `GET /api/v1/user/me` | 공개 OpenAPI의 `UserProfileResponse`에 `id`, `department`, `tenant`, `joinedTenants[]`가 있음 | 본인 프로필·가입 테넌트 목록 확인에 사용. `TenantResponse`에는 `id`, `name`, `type`, `parentId`가 있지만 `ancestors`는 정의되어 있지 않음 | +| `GET /api/v1/integrations/org-context` | 허용된 subtree의 `scope`, `tree`, `tenants[]`, 직접 소속된 `members[]` | 여러 허용 subtree를 조회하고 동일 사용자 UUID의 소속을 모으면 해당 조회 범위 안의 복수 소속을 찾을 수 있음 | + +`joinedTenants[]`는 가입 테넌트 목록을 받는 필드다. 모든 상위 조직까지 자동으로 포함하는 배열인지, 실제 로그인 사용자 응답에서 어떤 목록을 반환하는지는 별도 확인이 필요하다. 현재 Q&A는 `/user/me`를 호출하지 않고, 서버와 브라우저의 정규화 코드도 `joinedTenants`를 읽지 않는다. + +`/user/me`는 사용자 인증 문맥으로 호출하는 API다. Integration Key를 이 경로의 인증 수단으로 바꾸지 않는다. 기존 `AUTH_USERINFO_URL`을 무조건 `/user/me`로 교체해서도 안 된다. 기존 OIDC `sub`와 프로필의 canonical `id`를 대조하며 보충 정보를 결합해야 한다. + +조직 API 호출은 다음 계약을 사용한다. + +```http +GET https://sadmin.hmac.kr/api/v1/integrations/org-context?tenantId=<허용된-조회기준-UUID>&exposure=identifiers +X-Baron-Key-ID: <서버에 등록된 키 ID> +X-Baron-Key-Secret: <서버에 등록된 시크릿> +Accept: application/json +``` + +- OperationID: `baron.org-context.read`. +- `tenantId`, `tenantSlug` 중 정확히 하나를 지정한다. 조회 기준을 생략한 전체 조직 조회 계약은 없다. +- credential이 허용한 subtree 및 exposure 범위만 조회할 수 있다. 여러 조직에 겸직 중이어도 조회 범위 밖의 소속까지 확인된 것으로 처리하지 않는다. +- `schemaVersion` 예시는 `baron.org-context.v2`다. +- `members[].id`는 canonical 사용자 UUID이며 `members[]`는 해당 테넌트의 직접 소속 사용자 목록이다. +- `isPrimary`는 대표 소속 선택의 근거다. 복수 소속을 수집한 뒤 현재 테넌트 문맥과 대표 소속을 함께 판단한다. +- `members` exposure에도 canonical UUID가 포함된다. `identifiers`에는 추가로 전화번호가 포함되므로, 전화번호도 사용하는 현재 구현에서 `identifiers` 요청 자체가 잘못된 것은 아니다. +- 구성원의 회사 이메일이나 이름으로 사용자 ID를 대신 맞추지 않는다. + +## 현재 코드에서 재현된 매핑 오류 + +`src/org-context.js`의 `buildOrgDirectory()`는 모든 조회 기준 UUID를 분류 루트로 취급하고, 그 바로 아래 노드를 회사로 선택한다. `src/index.js`의 로그인 claim 정규화에도 같은 가정이 있다. + +조회 범위를 회사 자체로 설정하는 것은 API 계약상 가능한 요청이다. 하지만 아래처럼 회사에서 시작하는 응답을 현재 코드에 전달하면 회사가 한 단계 밀린다. + +```text +조회 기준: 총괄기획실&기술개발센터 +└─ IS3 + └─ 해당 사용자 UUID의 membership + +기대: 회사=총괄기획실&기술개발센터, 부서=IS3 +현재 코드의 재현 결과: 회사=IS3, 부서="", 상태=resolved +``` + +이 결과는 합성 UUID를 사용한 로컬 재현이며, 회사 기준 조회에 존재하는 코드 결함이다. 사용자 확인에 따르면 실제 조회 기준은 최상위 분류 조직이므로 **이 사례를 현재 스테이징 문제의 원인으로 확정할 수 없다**. + +반대로 `한맥가족 → 총괄기획실&기술개발센터 → IS3 → 사용자 membership` 계층을 현재 코드에 전달하면 회사와 부서가 기대대로 매핑된다. 이 경우도 로컬 합성 데이터로 확인했다. 실제 화면의 원인을 구분하려면 다음을 대조해야 한다. + +| 확인 지점 | 대조할 값 | 구분할 문제 | +| --- | --- | --- | +| Q&A 로그인 후 `/auth/session` | `user.orgContextStatus`, `company`, `department`, `companyTenantId`, `departmentTenantId`, `departmentPath` | 조직 조회 실패·사용자 연결 실패와 잘못된 조직 선택을 구분 | +| 조직 API의 해당 사용자 소속 | 조회 기준부터 membership 노드까지의 `id`, `name`, `parentId`, `type`, `orgUnitType`, `isPrimary` | 실제 계층과 대표 소속을 확인. 회사가 IS3의 상위 노드로 제공되는지 확인 | +| 해당 글의 API 응답 | `requester_affiliation`, `requester_department`, `tenant_name` | 현재 프로필은 정상이나 과거 저장 값 또는 표시 fallback이 잘못됐는지 구분 | + +전체 디렉터리, 키, 시크릿, 쿠키, access token을 공유할 필요는 없다. 서버 측 진단은 해당 사용자 UUID에 연결된 소속 경로와 정규화 결과만 대상으로 한다. + +추가로 확인한 코드 경로: + +- `normalizeClaims()`는 `tenants`와 `tenantIds`를 읽지만 `joinedTenants`는 읽지 않는다. 첫 번째 테넌트 선택도 대표 소속 보장을 대신할 수 없다. +- 로그인 claim의 `primaryTenant.ancestors`를 사용하지만 `/user/me`의 `TenantResponse` 계약에는 해당 필드가 없다. 전체 계층이 자동으로 들어온다고 기대할 수 없다. +- `egbim/app.js`의 `normalizeRemotePost()`는 회사 필드가 없으면 `tenant_name`까지 회사 후보로 사용한다. 그 값이 부서 이름이라면 `IS3`가 회사로 표시될 수 있다. +- 신규 글 저장 시 `requester_affiliation`과 `requester_department`에 서버가 계산한 값을 저장한다. 이미 잘못 저장된 글은 로그인 사용자 프로필만 고쳐도 자동으로 바로잡히지 않는다. + +## 필요한 후속 작업 + +1. 사용자가 확인한 최상위 분류 조직 기준을 유지한다. 실제 세션에서 `orgContextStatus=unavailable`을 확인했으므로 먼저 Worker에서 기록하는 정확한 `org_context_...` 오류를 확인하고 조회 실패를 해결한다. 이후 해당 사용자의 canonical UUID 및 회사·IS3 UUID를 원본 조직 응답에서 대조한다. +2. 조회 범위와 회사 판별 기준을 구분한다. API의 `type`, `orgUnitType`, `parentId`와 실제 계층을 확인하고, 의미를 구분할 수 없는 동일 타입 계층은 별도의 명시적 회사 기준을 사용한다. 조회 기준이라는 이유만으로 그 노드를 회사 후보에서 제외하지 않는다. +3. 모든 허용 subtree의 membership을 사용자 UUID로 모으고, 현재 인증 테넌트 문맥과 `isPrimary`로 소속을 선택한다. `/user/me`의 `joinedTenants`를 도입할 경우 기존 OIDC 식별자를 유지하면서 보충 정보로 결합한다. +4. 신규 저장에서 회사 `총괄기획실&기술개발센터`, 부서 `IS3`가 각각 올바른 필드로 나가는지 확인한다. 회사 기준 조회·분류 루트 조회·부서 기준 조회·겸직을 구분해 검증한다. +5. 기존 글 중 잘못 저장된 항목의 정정 범위를 별도로 확인한다. 현재 조회자의 조직을 다른 작성자의 글에 덮어쓰거나 과거 전보 이력을 현재 소속으로 일괄 대체하지 않는다. + +초기 확인에서는 조사만 진행했고, 추가 로그 확인 후 서비스 코드에 안전한 오류 코드 진단을 보완했다. 조직 소속과 기존 글 데이터는 변경하지 않았다. 실제 사용자 응답은 사용자가 제공한 `/auth/session` 결과만 확인했고, 사용자 token이나 Integration Credential로 직접 원본 조직 API를 조회하지 않았다. + +## 확인 자료 + +- [Org Context API Guide](sso_guides/org-context-api.md) +- [조직선택기 API 연동 계약](sso_guides/org-picker-integration.md) +- [Directory API Guide](sso_guides/directory-api.md) +- [RP Scope·Claims 설정](sso_guides/rp-integration.md) +- [배포 서버 OpenAPI](https://sadmin.hmac.kr/api/openapi.yaml): `UserProfileResponse`, `TenantResponse`, `OrgContextResponse`, `OrgContextTenant`, `OrgContextMember` 및 각 경로. 2026-10-30 직접 조회. diff --git a/docs/sso_guides/admin-operations-center.md b/docs/sso_guides/admin-operations-center.md new file mode 100644 index 0000000..b5fcea0 --- /dev/null +++ b/docs/sso_guides/admin-operations-center.md @@ -0,0 +1,121 @@ +# 운영 센터 사용법 + +AdminFront에서 **시스템 → 운영 센터**를 연다. 주소는 `/system/operations`다. System Role이 `super_admin`인 사용자만 메뉴와 API를 사용할 수 있다. Tenant Authority의 `owner`나 `admin`만 가진 사용자는 대상이 아니다. 역할의 의미는 [권한 컨텍스트](../../CONTEXT.md)를 따른다. + +## 시스템 상태 확인 + +시스템 상태 탭이 보이는 동안 5초마다 상태를 갱신한다. 인스턴스 ID, 빌드 정보, 수집 시각을 먼저 확인한다. 여러 서버를 운영한다면 각 응답은 요청을 처리한 서버의 상태이며 전체 서버의 합계가 아니다. 이전 응답이 남아 있어도 갱신 오류나 오래된 수집 시각이 보이면 현재 상태로 판단하지 않는다. + +- **Heap allocation**은 Go 객체가 현재 사용하는 힙이다. **Heap reserved**는 런타임이 확보한 힙 공간이다. +- **RSS**는 운영체제가 집계한 프로세스의 상주 메모리다. Go 힙과 같지 않다. +- **Total allocated**는 시작 이후의 누적 할당량이다. 현재 메모리 사용량이나 누수 크기가 아니다. 초당 할당량과 힙 추이를 함께 확인한다. +- **Goroutines**는 현재 goroutine 수다. 같은 부하에서 계속 늘어나는지 관찰한다. +- GC 정지와 스케줄러 대기 histogram은 수집 구간별 bin의 발생 횟수다. 첫 수집에는 이전 표본이 없어 구간 통계가 없다. 상한이 없는 마지막 bin은 `null`로 표현한다. +- 지원하지 않는 RSS·cgroup 항목이나 수집이 꺼진 항목은 사용할 수 없음으로 표시한다. 0으로 해석하지 않는다. + +최근 약 15분의 런타임 이력은 인스턴스 메모리에 보관되며 재시작하면 사라진다. 장기 관찰에는 별도의 시계열 저장소가 필요하다. + +## 의존 서비스 확인 + +Backend 연결 풀에서는 사용 중 연결 수, 최대 연결 수, 누적 대기 횟수와 시간을 확인합니다. 서버 통계는 Backend PostgreSQL, Backend ClickHouse, Ory 로그 ClickHouse로 구분합니다. 이미지 배포는 하나의 ClickHouse 서버 안에 업무 DB와 Ory 로그 DB를 나누므로 두 카드의 서버 전체 수치는 같을 수 있습니다. 수집 실패는 대상별로 표시합니다. 풀 통계 조회 성공만으로 원격 서비스의 정상 상태를 판단하지 마십시오. + +PostgreSQL에서는 연결 수와 한도, 실행 쿼리, 열린 채 대기 중인 트랜잭션, 잠금 대기와 DB 크기를 확인합니다. 커밋·롤백·교착 상태·임시 데이터·캐시 적중·디스크 읽기는 각 DB의 통계 초기화 이후 누적값입니다. 화면의 초기화 시각은 가장 최근 DB 초기화 시각입니다. 서버 전체 통계에는 관측 계정의 `pg_read_all_stats` 권한과 대상 DB의 `CONNECT` 권한이 필요합니다. 권한을 자동으로 부여하지 않으며 부족하면 사용할 수 없음으로 표시합니다. + +Ory PostgreSQL은 격리된 DB 네트워크에 두며 운영센터에서 직접 조회하지 않습니다. Ory 서비스 카드에서 Kratos Public·Admin, Hydra, Keto, Oathkeeper의 준비 상태와 검사 응답 시간을 확인합니다. 검사 응답 시간은 readiness API의 응답 시간입니다. 실제 업무 요청의 오류와 지연은 아래 요청 통계에서 함께 확인합니다. + +이미지 배포 번들은 Ory 로그 스키마를 초기화하고 Vector 수집기를 시작합니다. 수집기는 INSERT 전용 계정으로 Oathkeeper 파일 로그를 저장하고 backend는 SELECT 전용 계정으로 읽습니다. 계정 설정은 번들에서 생성하며 별도 비밀번호 입력은 필요하지 않습니다. 로그 조회 연결은 저장소가 일시적으로 중단되어도 다음 조회에서 다시 연결합니다. 수집기는 볼륨에 둔 디스크 버퍼와 전송 확인으로 재시작에 대비하며, 원본에 유효한 시각이 있으면 이를 보존합니다. 재전송 중 같은 기록이 다시 전달될 수 있습니다. + +ClickHouse의 연결 수, 메모리, 실행 쿼리와 병합 수는 서버 전체 수치입니다. 저장공간과 데이터 조각 수는 접속 DB의 활성 MergeTree 데이터 조각만 포함합니다. 실행 쿼리 수에는 통계를 수집하는 쿼리도 포함합니다. + +Redis에서는 현재 사용량, 시작 이후 최대 사용량, 상주 메모리, 데이터 메모리와 설정 한도를 확인합니다. 값은 Redis 서버의 모든 DB를 포함합니다. 최대 사용량은 누적 할당량이 아니며 캐시를 비워도 초기화되지 않습니다. 설정 한도 0은 사용량 0이 아니라 별도 한도가 없다는 뜻입니다. + +Identity 캐시 비우기는 기존 identity cache API를 사용합니다. 확인창에서 동의하면 해당 캐시 키만 삭제하고 메모리를 다시 조회합니다. 로그인 진행 상태와 중복 실행 방지 키는 유지합니다. 캐시가 복구될 때까지 로그인 조회가 실패할 수 있습니다. 세대별 projection 데이터 전체를 삭제하지 않으므로 서버 전체 메모리 반환을 보장하지 않습니다. + +## 성능 비교 이력 가져오기 + +정상 실행에서 수집한 baseline과 candidate의 원시 지연 시간 배열을 JSON 파일로 준비해 성능 이력 화면에서 가져온다. 서버가 배열을 정렬해 nearest-rank 방식으로 p50·p95·p99를 다시 계산한다. 예를 들어 30개 표본의 p95는 정렬한 29번째 값이다. 기존에 계산한 분위수만 보내는 형식은 받지 않는다. + +한 파일의 최상위 객체에 `name`, `baseline`, `candidate`, `p95BudgetRatio`를 넣는다. `baseline`과 `candidate`는 각각 실행 객체이며 각 실행의 `metrics`는 측정 항목 객체의 배열이다. `fixture`의 사용자·테넌트·actor 수는 정수이고 `hash`는 문자열이다. 입력 구조를 그대로 사용할 수 있는 [완전한 JSON 예제](../../adminfront/tests/fixtures/operations-performance-import-archived-c1.json)를 먼저 내려받아 확인한다. 새 비교에는 실행 식별자와 측정값, 소스·이미지·fixture 정보를 해당 실행의 실제 값으로 바꾼다. + +각 실행에는 다음 정보가 필요하다. 정확한 필드 정의는 [성능 이력 API 코드](../../backend/internal/handler/operations_performance_handler.go)를 참조한다. + +| 항목 | 입력 조건 | +| --- | --- | +| 실행 식별 | `runId`, 소스 식별 해시 `sourceRevision`, 불변 이미지 ID `imageId` | +| 소스 | Git revision 40자리 또는 소스 manifest SHA-256 64자리. manifest 해시는 별도 원본과 함께 보관한다. | +| 이미지 | `sha256:` 뒤에 64자리 16진수 | +| 환경 | `local`, `test`, `staging` 중 하나 | +| 측정 방식 | `normal-http` 또는 `normal-browser` | +| fixture | `identities`, `tenants`, `actors`, 전체 측정 데이터 조건의 SHA-256 `hash` | +| 동시성 | 양의 정수 `concurrency` | +| 진단 여부 | `diagnosticsEnabled`, `cpuProfileEnabled`, `traceEnabled`를 모두 명시적으로 `false` | +| 측정 항목 | 고유한 이름, `warmupCount` 5 이상, 양수 밀리초 배열 `samplesMs` 30개 이상 | +| 처리량 입력 | 선택 항목 `measuredElapsedMs`는 워밍업을 제외한 측정 구간의 실제 경과 시간. 개별 표본의 최댓값보다 작을 수 없음 | +| 오류 | `httpFailures`와 `transportFailures`는 모두 0 | +| 허용 비율 | `p95BudgetRatio` 기본 1.1. 더 엄격한 양의 값으로 낮출 수 있으며 1.1을 초과할 수 없음 | + +baseline과 candidate의 환경·측정 방식·fixture·동시성·측정 항목·워밍업 횟수가 일치해야 한다. 프로파일링을 켠 실행을 정상 실행과 비교할 수 없다. 파일은 1 MiB 이하, 실행당 항목은 20개 이하, 항목당 표본은 10,000개 이하다. + +비교 상태가 `passed`라는 것은 모든 항목의 candidate p95가 설정한 비율 이내라는 뜻이다. p99나 절대 응답 시간 목표를 달성했다는 뜻은 아니다. p99와 원시 표본도 확인한다. 오류가 발생한 실행은 비교용 성공 표본에서 조용히 제외하지 말고 측정 실행의 별도 증거로 남긴다. + +처리량은 표본 수를 실제 측정 경과 시간으로 나눈 requests/second다. `measuredElapsedMs`가 없으면 사용할 수 없음으로 표시한다. 지연 시간의 역수로 처리량을 추정하지 않는다. 같은 조건의 여러 측정 구간을 합칠 때는 표본 배열을 합치고 해당 구간의 경과 시간도 합산한다. + +이력은 PostgreSQL에 저장된다. 동일 사용자가 같은 내용을 재전송하면 기존 기록을 반환한다. 원본 실행의 진위를 서버가 인증하는 기능은 아니므로 기록은 `operator_imported` 출처로 표시한다. 소스·이미지·fixture manifest와 실행 로그는 원래의 이슈나 Actions artifact에도 보관한다. + +[실제 측정값 예제](../../adminfront/tests/fixtures/operations-performance-import-archived-c1.json)는 이전 검색 개선 이슈의 HTTP 측정을 가져온 것이다. 출처는 [#2430](https://gitea.hmac.kr/baron/baron-sso/issues/2430)에서 확인한다. 운영 센터 자체의 새 성능 측정 결과로 해석하지 않는다. + +## 수집 설정 + +일반 운영 지표는 기본 활성화다. 수집 비용 비교나 수집 중지가 필요하면 서버 실행 인수에 `--operations-metrics=false`를 지정한다. UI에서 프로세스 실행 설정을 바꾸지는 않는다. + +빌드·인스턴스 정보는 [`.env.sample`](../../.env.sample)의 `BARON_INSTANCE_ID`, `BARON_RELEASE_VERSION`, `BARON_RELEASE_GIT_COMMIT`을 사용한다. 빌드 정보가 없으면 `unknown`으로 표시한다. 여러 인스턴스에 같은 ID를 지정하지 않는다. + +CPU·힙·스택 진단은 기본 비활성화입니다. 일반 빌드에서도 모든 환경에서 `--operations-diagnostics`로 켤 수 있습니다. 끄려면 플래그를 생략하거나 `--operations-diagnostics=false`를 지정하고 backend를 다시 시작하십시오. UI는 시작 플래그를 변경하지 않습니다. 기존 검색 구간 진단 `--profile-search`의 빌드·환경 제한은 별도로 유지합니다. + +진단 artifact에는 함수명과 실행 경로 같은 내부 정보가 포함될 수 있다. 해당 작업을 조사하는 관리자에게만 전달하고 공개 이슈에 원본을 첨부하지 않는다. + +## 요청 지연의 위치 좁히기 + +기본 정렬은 누적 소요 시간 내림차순입니다. 오류 열 제목을 누르면 오류 횟수 내림차순으로 바뀝니다. HTTP와 Ory 표는 정렬 선택을 각각 유지합니다. 등록 endpoint에 도달하지 않은 요청은 `unmatched`로 집계하며 실제 요청 URL은 표시하지 않습니다. 기존 `/` 또는 `/api/v1` 미들웨어 경로로 집계되던 미등록 요청도 여기에 포함됩니다. + +의존 서비스 화면의 요청 통계에서 같은 인스턴스에 반복되는 HTTP route의 횟수·오류 수·지연 분포를 확인한다. route는 등록된 경로 템플릿으로 집계하며 사용자 ID, 검색어, 토큰을 label에 넣지 않는다. 운영 센터 자체의 폴링은 이 통계에서 제외한다. HTTP series는 256개와 초과분 집계 1개로 제한한다. + +Kratos·Keto·Hydra 통계는 서비스와 HTTP method 단위다. 해당 클라이언트의 요청 시작부터 응답 헤더 수신까지를 측정하며 본문 다운로드·JSON 해석 시간은 포함하지 않는다. 따라서 API 전체 지연과 동일하게 해석하지 않는다. 오류는 transport 실패 또는 HTTP 400 이상이다. histogram의 각 bin은 해당 구간의 누적 횟수이고, 모든 bin의 합이 요청 횟수다. 상위 경계까지의 누적 합 형식이 아니다. + +예를 들어 API 응답이 느려졌는데 Ory 지연은 안정적이고 DB 대기가 늘었다면 DB 연결 경합을 우선 조사한다. Ory 지연이 같이 늘면 해당 의존 서비스와 네트워크 구간을 확인한다. 통계만으로 원인을 확정하지 말고 진단 artifact나 독립된 요청 기록과 대조한다. + +## 작업과 진단 + +작업 화면의 읽기 전용 점검은 DB와 Redis에 실제 연결 확인을 수행한다. 점검에 성공하면 `succeeded`, 실패하면 `failed`로 끝난다. 업무 데이터나 권한 관계는 수정하지 않는다. + +작업은 이 요청을 받은 인스턴스에서 실행된다. 취소 버튼은 협력적 취소 요청을 보낸다. 작업이 `canceled`로 끝났는지 다시 확인한다. 이미 끝난 작업은 취소할 수 없다. 프로세스 자체나 임의의 goroutine을 강제로 종료하는 기능이 아니다. + +진단을 허용한 인스턴스에서는 스택을 확인하거나 비동기 진단 작업을 시작할 수 있다. CPU 수집 시간은 기본 5초이며 1~30초다. 같은 인스턴스에서는 진단 작업 하나만 동시에 수행한다. 완료된 작업에서 ZIP을 내려받으면 전후 goroutine 스택과 manifest, 선택한 CPU·힙 profile을 확인할 수 있다. + +```sh +# 내려받은 ZIP을 별도 작업 디렉터리에 푼 뒤 실행 +go tool pprof -top cpu.pprof +go tool pprof -top -sample_index=inuse_space heap.pprof +# 누적 할당량을 조사할 때 +go tool pprof -top -sample_index=alloc_space heap.pprof +``` + +스택에서는 오래 대기하는 함수와 반복되는 호출 경로를 확인한다. CPU profile에서는 CPU를 소비한 함수를 확인한다. Heap의 `inuse_space`는 수집된 프로파일 기준으로 살아 있는 객체의 샘플링된 메모리이고 `alloc_space`는 누적 할당량이다. 둘을 메모리 누수 크기로 단정하지 않는다. 프로파일링 중 측정한 응답 시간을 정상 실행의 성능 비교에 넣지 않는다. + +작업 목록과 artifact는 메모리에 보관된다. 최대 50개 작업, artifact 하나당 최대 16 MiB, 보관 artifact 총합 32 MiB로 제한한다. artifact는 30분 후 만료되며 용량이 부족하면 먼저 제거될 수 있다. 작업 기록은 1시간 후 제거될 수 있고 재시작하면 모두 사라진다. 여러 인스턴스 사이에서 작업과 다운로드를 공유하지 않으므로 같은 인스턴스로 접근해야 한다. + +로드밸런서 뒤에 여러 backend가 있다면 운영용 AdminFront의 `API_PROXY_TARGET`을 조사할 단일 backend 주소로 지정해 실행한다. [AdminFront 서버](../../adminfront/scripts/serve-prod.mjs)는 이 값을 API 프록시 대상으로 사용한다. 접속 후 시스템 상태를 두 번 새로 고쳐 인스턴스 ID가 같은지 확인한 뒤 작업을 시작한다. 작업 완료·취소·다운로드까지 같은 AdminFront와 프록시 대상을 유지한다. 이 기능 자체가 로드밸런서의 세션 고정이나 인스턴스 선택을 제공하지는 않는다. + +## 큐의 실패 항목 처리 + +큐 화면에서 대기·실패 수와 해당 상태 항목의 가장 오래된 생성 시각을 확인한다. 이 시각은 마지막 실패나 재시도 시각과 다르다. 작업·큐 탭은 화면이 보이는 동안 10초마다 갱신하며, 재시도 후에는 새로고침 버튼으로 즉시 확인할 수 있다. 실패 목록은 큐별 최대 20개만 표시하므로 표시된 행 수가 전체 실패 수와 같지 않을 수 있다. + +실패 원인은 timeout, 의존 서비스 연결 실패, 권한 거부, 요청 제한, 기타 relay 실패로 분류한다. 오류 문자열을 고정 범주로 분류한 조사 단서이며 원인을 확정하는 진단은 아니다. 개별 요청의 payload나 외부 서비스 오류 원문은 표시하지 않는다. 정확한 원인은 해당 outbox ID로 내부 로그를 조회해 확인한다. + +Keto 실패 항목은 원인을 해결한 뒤 해당 행의 재시도를 선택한다. 서버는 기존 권한 변경과 같은 subject 잠금 안에서 최신 generation과 상태를 다시 확인한 후 `failed`를 `pending`으로 바꾼다. 이미 대기 중인 항목의 반복 요청은 중복 작업을 만들지 않는다. 이후 실제 relay가 완료하는지는 큐 상태를 새로 조회해 확인한다. 재시도 수락은 외부 반영 완료가 아니다. + +오래된 generation이나 더 이상 재시도할 수 없는 항목은 거부한다. 선행 작업이 끝나지 않았거나 의존 서비스 장애가 남아 있으면 대기가 계속될 수 있다. WORKS 복구는 [WORKS 연동 Runbook](../runbooks/worksmobile.md)의 해당 작업 흐름을 따릅니다. + +진단 탭의 큐 재시도 관리 버튼으로 작업·큐 탭을 여십시오. Keto와 WORKS의 대기·실패 항목에서 재시도 중단을 선택하고 확인하십시오. 중단한 항목은 기록과 선행 관계를 유지하며 자동·수동 재시도에서 제외됩니다. 중단 수는 별도로 표시합니다. 이미 처리 중이거나 완료된 작업은 중단할 수 없습니다. 선행 작업을 중단하면 후속 작업이 계속 대기할 수 있습니다. + +중단을 성공 처리로 간주하지 않습니다. 이 화면은 중단 해제나 전체 큐 삭제를 제공하지 않습니다. diff --git a/docs/sso_guides/adminfront.md b/docs/sso_guides/adminfront.md new file mode 100644 index 0000000..1e401a0 --- /dev/null +++ b/docs/sso_guides/adminfront.md @@ -0,0 +1,124 @@ +# AdminFront 운영 가이드 + +## 대상 + +Tenant와 사용자를 조회·관리하고 권한, WORKS, API Key, 데이터 정합성 화면을 사용하는 운영자를 위한 가이드다. 실제로 보이는 menu와 action은 actor의 System Role, Tenant relation, 기능별 capability에 따라 달라진다. + +## 권한을 읽는 법 + +- `super_admin`은 전역 System Role이다. +- Tenant owner/admin과 기능별 view/edit/manage는 Tenant 범위의 relation이다. +- 사용자의 소속과 관리자가 관리할 수 있는 Tenant 집합은 서로 다르다. +- 메뉴가 보이거나 button이 활성화되었다는 사실만으로 mutation이 허용되지는 않는다. Backend가 요청 시점의 권한과 대상을 다시 검사한다. + +용어는 [`CONTEXT.md`](../../CONTEXT.md), 관계 원칙은 [Tenant and Authorization Principles](../principles/tenancy-and-authorization.md)를 따른다. + +시스템 상태, 의존 서비스, 작업·진단 및 성능 비교는 super admin 전용 [운영 센터 사용법](admin-operations-center.md)을 참고한다. + +## 긴 목록 탐색 + +긴 표에 내부 스크롤이 있으면 표 영역 안에서 행을 탐색합니다. 열이 화면보다 넓으면 같은 영역에서 가로로 스크롤합니다. 표 헤더는 스크롤 영역의 상단에 유지됩니다. 페이지 스크롤을 사용하는 표는 앱 헤더 아래에 표 헤더를 유지합니다. 추가 조회 버튼이 보이면 선택하여 다음 목록을 불러옵니다. 검색과 필터는 화면에서 제공하는 조회 조건으로 사용합니다. + +사용자·Tenant 목록에서 이름이나 날짜로 정렬하려면 검색어를 지우고 열 제목을 선택하세요. 검색어를 입력하면 관련도 순서로 표시합니다. 검색어를 지우면 검색 전 정렬로 돌아갑니다. 검색 정렬 정책은 [UI Design Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/UI-Design-Policy#fuzzy-검색의-기본-정렬)를 따릅니다. + +## Tenant 작업 + +검색어가 없으면 목록의 기본 분류 순서는 한맥가족 → 파트너사 → 일반회사 → 공공기관 → 교육/학생 → 해외기관 → Personal입니다. 이때 트리와 평면 보기는 같은 기본 순서를 사용하고, 하위 Tenant는 이름순으로 표시합니다. 사용자가 열 정렬을 선택하면 해당 정렬을 우선 적용합니다. + +구현은 [정렬 코드](../../adminfront/src/lib/tenantTree.ts)와 [목록 화면](../../adminfront/src/features/tenants/routes/TenantListPage.tsx)에 있습니다. [단위 테스트](../../adminfront/src/lib/tenantTree.test.ts)와 [화면 회귀 테스트](../../adminfront/tests/tenant-default-order.issue-2206.spec.ts)는 정렬 동작을 검증합니다. + +1. Tenant 목록에서 이름, slug, 유형, 상위 Tenant를 확인한다. +2. `COMPANY`의 사업자등록번호는 선택 입력이며 `000-00-00000` 형식으로 표시된다. 번호 검색 결과가 범위 안이면 기존 회사를 확인하고, 범위 밖 일치이면 상세 없이 검토 요청을 남긴다. +3. 하위 Tenant 생성 시 선택 가능한 parent만 사용한다. 부모와 자식 `COMPANY`가 각각 사업자등록번호를 갖는 것은 정상이다. +4. CSV import/export에서는 `type` 다음의 `business_registration_number` 컬럼을 사용한다. import는 기존 영문·한글 alias도 읽지만 export는 canonical header만 출력한다. legacy CSV에 이 컬럼이 없으면 기존 번호를 변경하지 않으며, 컬럼이 있고 셀이 비어 있을 때만 명시적 clear로 처리한다. export는 Tenant page permission을 가진 actor에게만 열리고 Backend가 계산한 Tenant scope를 벗어나지 않는다. +5. Organization Read/Write/Manage 표시를 단일 저장 role로 해석하지 않는다. +6. child create와 delete capability는 독립적으로 거부될 수 있다. +7. seed 또는 protected Tenant는 일반 Tenant와 삭제 조건이 다르므로 화면 경고와 Backend 결과를 따른다. +8. Exact Super Admin은 Tenant 상세의 인증 정책 화면에서 AAL2 강제를 즉시 켜거나 끌 수 있다. 사용자의 모든 canonical 소속 중 가장 높은 요구사항이 적용되며, 재활성화할 때 과거 대상자를 제외하지 않는다. +9. 정책을 꺼도 이미 등록된 TOTP credential은 자동 삭제하지 않는다. 개인이 선택해 등록한 TOTP도 계속 사용할 수 있다. + +## 사용자 작업 + +1. 사용자 목록의 현재 Tenant filter와 조회 범위를 확인한다. +2. 계정 생성, profile 변경, Membership 추가·제거, 대표 Tenant 변경, status 변경을 서로 다른 작업으로 취급한다. +3. bulk action 전에 선택 대상과 action 종류를 다시 확인한다. +4. Membership 제거를 계정 삭제로 오인하지 않는다. +5. identity 영구 삭제는 제한된 별도 command이며 self/protected guard를 우회하지 않는다. +6. Exact Super Admin 전용 `Stealth 식별자` 화면에서만 `stealth` 노출 정책을 관리한다. Stealth 식별자는 일반 검색, CSV, 조직도, org-context, AdminFront 통계, WORKS projection에서 제외한다. +7. `identity_visibility_policies`가 visibility 권한 원장이고 `users.identity_visibility`는 같은 transaction에서 갱신하는 조회 projection이다. 기존 public identity의 policy row가 없으면 canonical `public`으로 해석한다. 두 값이 불일치하면 더 제한적인 값을 적용하고, 저장소 장애는 public으로 완화하지 않고 요청을 닫는다. 이 필터는 AdminFront 일반 통계의 기존 집계 의미를 바꾸지 않는다. + +### 사용자 소속 선택 + +루트는 조직 계층의 최상위 테넌트입니다. +한맥가족, 파트너사, 일반회사, 공공기관, 교육/학생, 해외기관은 같은 검색 트리 선택기를 사용합니다. +사용자 추가와 수정 화면 모두 테넌트 이름이나 식별자(slug)로 검색할 수 있습니다. + +선택기는 서버가 허용한 테넌트만 표시합니다. +하위 조직을 검색하면 응답에 포함된 상위 조직도 함께 표시합니다. +상위 조직이 권한 범위에 없으면 해당 하위 조직부터 표시합니다. +소속 선택에는 OrgFront 로그인이나 조직도 조회 권한이 필요하지 않습니다. + +1. 사용자 추가 또는 수정 화면에서 소속 루트 탭을 선택하세요. +2. 한맥가족 탭에 소속 행이 없으면, 추가 버튼을 누르세요. +3. 한맥가족 탭이면, 소속 행의 선택 버튼을 누르세요. +4. 다른 조직 탭이면, 대표 소속 항목의 버튼을 누르세요. +5. 필요하면 검색창에 테넌트 이름이나 식별자를 입력하세요. +6. 트리에서 허용된 테넌트를 선택하세요. +7. 화면에 반영된 테넌트 이름을 확인하세요. +8. 사용자 생성 또는 저장 버튼을 누르세요. + +Personal은 전용 테넌트를 자동으로 사용하므로 트리에서 선택하지 않습니다. +한맥가족의 여러 소속을 지정하려면 소속 행을 추가하세요. +저장할 소속은 하나의 루트 아래에 속해야 합니다. + +권한을 처음 불러오는 중이거나 조회에 실패하면 선택할 수 없습니다. +검색어가 없는데 선택기가 비어 있으면 현재 루트에서 선택할 수 있는 테넌트가 없는 상태입니다. +서버는 저장할 때 권한을 다시 검사합니다. + +권한 원칙은 [Tenant and Authorization Principles](../principles/tenancy-and-authorization.md)를 따릅니다. +선택기 구현은 [MembershipTenantPicker](../../adminfront/src/features/users/components/MembershipTenantPicker.tsx)에 있습니다. + +## 권한 위임 + +- 읽기, 편집, 관리 단계를 대상 기능과 Tenant scope별로 부여한다. +- `self`와 `subtree`를 구분한다. +- preview의 현재 상태와 결과 상태를 확인한 뒤 저장한다. +- accepted 또는 pending 상태를 effective 완료로 표시하지 않는다. +- 저장 후 권한 목록이 원장 상태로 수렴했는지 다시 확인한다. + +## 데이터 정합성 + +`데이터 정합성` 화면의 기본 조회는 진단이다. repair 후보가 표시되어도 자동 삭제하지 않는다. + +1. 실패 항목과 candidate의 UUID·Tenant·사유를 확인한다. +2. 원장 장애인지 실제 orphan인지 구분한다. +3. repair가 필요하면 [Data Integrity Runbook](../runbooks/data-integrity.md)을 따른다. +4. Kratos/Keto 조회 실패 중에는 삭제를 재시도하지 않는다. +5. 사업자등록번호 검토 요청은 번호·요청자·대상 부모·전역 일치 Tenant를 확인하고 필수 의견과 함께 처리한다. +6. 중복 회사 병합은 exact duplicate 그룹 전체에서 생존 Tenant와 모든 source Tenant를 확인한 뒤 plan을 생성하고, 생존 slug와 사유를 입력해 적용한다. 여러 그룹 중 일부만 실패하면 완료 영수증은 유지하고 실패 operation만 같은 `Idempotency-Key`로 재시도한다. +7. 일반 삭제가 거부된 경우에만 `super_admin` 화면에서 강제 영구 삭제를 선택한다. plan의 Tenant 이름·slug·parent와 orphan Membership, Login ID, User Group, domain, Shared Link, Developer Request, lifecycle schedule, Client Consent reference, credential 수, blocker를 확인하고 exact slug와 사유를 입력한다. +8. 강제 삭제 영수증에 외부 cleanup residual이 남으면 재시도하거나, 책임자가 사유를 남겨 잔여 문제를 수용하기 전까지 완료로 취급하지 않는다. +9. 브라우저 reload나 담당자 변경으로 미완료 병합·강제 삭제가 남으면 operation 복구 목록에서 대상을 확인하고 새 감사용 `Idempotency-Key`와 복구 사유로 재개한다. + +## 사용자를 생성한 관리자 찾기 + +1. 감사 로그 화면에서 **대상 UUID**에 생성된 사용자의 UUID 전체를 입력한다. **수행자 UUID**는 작업한 관리자 ID이므로 대상 사용자와 구분한다. +2. 작업에 `CREATE_USER`를 입력해 생성 업무 이벤트를 찾는다. `creat_user` 같은 한 글자 오타도 허용하지만 UUID는 전체 값이 정확히 일치해야 한다. +3. 행의 수행자 ID와 대상을 확인하고 상세의 `operation_id`, `commit_outcome`을 대조한다. 사용자 삭제 후에도 이 생성 이벤트는 조회할 수 있다. + +액션 텍스트는 부분 문자열·초성·한 글자 오타로 검색할 수 있다. 검색 결과는 [공통 fuzzy 검색 순서](https://gitea.hmac.kr/baron/baron-sso/wiki/UI-Design-Policy#fuzzy-검색의-기본-정렬)를 따르고 추가 조회에도 같은 조건을 유지한다. 텍스트 검색을 지우면 기존 시간순 조회로 돌아간다. + +`USER_CREATED`는 실제 생성 commit 기록이며 HTTP 요청의 성공·실패 로그와 구분한다. 일괄 요청의 일부가 실패했어도 commit된 사용자마다 생성 이벤트가 남는다. `creation_audit_pending` 응답은 생성 후 감사 전달이 지연된 상태이므로 새 키로 생성하지 말고 기존 `Idempotency-Key`를 유지한다. API 계약은 [OpenAPI](../../backend/docs/openapi.yaml)를 따른다. + +`CLIENT_EVENT`의 `received`는 클라이언트 보고를 수신했다는 뜻이다. 상세의 `client_status`가 `success`여도 서버에서 사용자 생성이 완료됐다는 증거로 사용하지 않는다. + +이전 버전의 생성 로그에 대상 UUID가 없으면 해당 UUID 검색으로 찾을 수 없다. 이름과 시각만 일치하는 로그를 UUID로 직접 연결된 증거처럼 취급하지 않는다. + +## 다른 운영 화면 + +- 운영 센터의 런타임, 의존성, 큐, 작업, 진단, 성능 비교: [운영 센터 사용법](admin-operations-center.md) +- WORKS 사용자 상태와 동기화: [WORKS 인사 가이드](works-hr.md) +- Integration credential: [Directory API 가이드](directory-api.md) +- RP와 DevFront: [RP 통합 가이드](rp-integration.md) +- Exact Super Admin bootstrap과 credential reset: [Account Remediation Runbook](../runbooks/account-remediation.md) +- 정책 정본: [Gitea Wiki](https://gitea.hmac.kr/baron/baron-sso/wiki) diff --git a/docs/sso_guides/api-errors.md b/docs/sso_guides/api-errors.md new file mode 100644 index 0000000..913e718 --- /dev/null +++ b/docs/sso_guides/api-errors.md @@ -0,0 +1,2869 @@ +# API 오류 코드와 조치 + +오류 응답의 `code`와 같은 항목을 찾으세요. +영문 설명은 API 오류 코드 등록부의 원문입니다. + +이 문서는 오류 코드 등록부와 한국어 조치 자료에서 생성합니다. +등록부는 `backend/internal/response/error_code_registry.go`에 있습니다. +한국어 조치 자료는 `backend/cmd/error-code-registry/error_guidance.json`에 있습니다. + + + +## `aal2_required` + +A higher authentication assurance level is required. + +현재 인증 수준이 요청에 필요한 수준에 도달하지 않았습니다. + +로그인 화면에서 요구한 추가 인증을 완료해 주세요. 인증을 마치면 원래 작업 화면으로 돌아가 주세요. + + + +## `access_denied` + +The requested operation was denied. + +서버가 현재 요청의 실행을 거절했습니다. 이 코드만으로 거절한 정책을 확정할 수 없습니다. + +응답의 상세 사유와 요청 대상을 확인해 주세요. 허용된 작업이 필요하면, 관리자에게 권한 검토를 요청해 주세요. + + + +## `account_remediation_required` + +The account must complete remediation before continuing. + +계정에 필요한 보완 절차가 남아 있어 다음 단계로 진행할 수 없습니다. + +응답이 안내한 계정 보완 화면에서 필요한 절차를 완료해 주세요. 절차를 완료할 수 없으면, 요청 식별자와 함께 지원 담당자에게 문의해 주세요. + + + +## `admin_only_field` + +The field is available only to administrators. + +요청에 관리자만 사용할 수 있는 필드가 포함되어 있습니다. + +일반 사용자 요청에서 해당 필드를 제외해 주세요. 그 필드 변경이 필요하면, 권한이 있는 관리자에게 요청해 주세요. + + + +## `ambiguous_tenant_selector` + +The request selects more than one tenant. + +요청의 Tenant 선택 값이 하나의 대상을 가리키지 않습니다. + +경로와 요청 본문의 Tenant 선택 값을 비교해 주세요. API 계약에 맞는 단일 Tenant를 지정해 주세요. + + + +## `application_profile_immutable` + +The application profile cannot change after creation. + +생성한 애플리케이션의 유형을 변경하려고 했습니다. + +기존 applicationProfile 값을 유지해 주세요. 다른 유형이 필요하면, 서비스 관리자에게 별도 애플리케이션 등록을 요청해 주세요. + + + +## `approval_state_unavailable` + +The approval state is unavailable. + +서버가 로그인 승인 상태를 조회하거나 저장하지 못했습니다. 승인 거절이나 계정 부재가 확정된 상태는 아닙니다. + +승인 요청을 중복 생성하기 전에 기존 로그인 진행 상태를 확인해 주세요. 문제가 계속되면, 요청 식별자를 지원 담당자에게 전달해 주세요. + + + +## `audit_unavailable` + +The audit service is unavailable. + +요청에 필요한 감사 기록 기능을 사용할 수 없습니다. + +서비스 관리자에게 감사 저장소 상태 확인을 요청해 주세요. 변경 요청이면, 기존 처리 결과를 먼저 확인해 주세요. + + + +## `authentication_upstream_failed` + +The authentication service failed to complete the request. + +연결된 인증 서비스가 요청을 완료하지 못했습니다. + +현재 로그인 상태와 응답의 다음 조치를 먼저 확인해 주세요. 문제가 계속되면, 요청 식별자를 서비스 관리자에게 전달해 주세요. + + + +## `authorization_pending` + +The authorization request is waiting for user action. + +사용자의 승인 등 필요한 동작이 끝나지 않아 인가 요청이 대기 중입니다. + +사용자에게 표시한 승인 화면에서 필요한 동작을 완료해 주세요. 상태 조회는 서버가 안내한 간격을 지켜 주세요. + + + +## `authorization_request_unavailable` + +The authorization request is unavailable. + +인증 서비스가 현재 인가 요청을 사용할 수 없다고 응답했습니다. 계정이 없다는 뜻은 아닙니다. + +기존 인가 요청을 다시 사용하지 마세요. 공식 서비스에서 새 로그인 절차를 시작해 주세요. + + + +## `bad_gateway` + +The upstream service returned an invalid gateway response. + +중간 서버가 연결된 서비스에서 유효한 응답을 받지 못했습니다. + +요청 식별자로 연결 서비스와 중간 서버의 상태를 확인해 주세요. 변경 요청은 처리 결과 확인 없이 반복하지 마세요. + + + +## `bad_request` + +The request is invalid. + +요청의 형식이나 값이 해당 API의 입력 조건에 맞지 않습니다. + +응답의 필드 오류와 API 요청 형식을 비교해 주세요. 지적된 값을 수정한 뒤 제출해 주세요. + + + +## `bound_courier_binding_unavailable` + +The bound courier binding is unavailable. + +로그인 요청과 인증 메시지를 연결하는 서버 데이터를 사용할 수 없습니다. + +기존 로그인 요청의 진행 상태를 먼저 확인해 주세요. 요청 식별자를 담당자에게 전달하고 연결 데이터 복구 여부를 확인해 주세요. + + + +## `bound_courier_code_missing` + +The bound courier code is missing. + +연결된 로그인 요청에 발송할 인증 코드가 없습니다. + +같은 발송 요청을 반복하기 전에 로그인 요청 상태를 확인해 주세요. 담당자에게 인증 코드 생성 단계의 확인을 요청해 주세요. + + + +## `bound_courier_expired` + +The bound courier request expired. + +인증 메시지에 연결된 로그인 요청의 유효 시간이 지났습니다. + +만료된 요청의 코드 사용을 중단해 주세요. 로그인 화면에서 새 인증 절차를 시작해 주세요. + + + +## `bound_courier_identity_mismatch` + +The bound courier identity does not match. + +인증 메시지에 연결된 사용자와 현재 로그인 요청의 사용자가 다릅니다. + +현재 로그인하려는 계정과 처음 인증을 요청한 계정을 확인해 주세요. 올바른 계정으로 로그인 절차를 다시 시작해 주세요. + + + +## `bound_courier_in_progress` + +The bound courier request is already in progress. + +같은 로그인 요청의 인증 메시지 처리가 이미 진행 중입니다. + +현재 발송 결과를 먼저 확인해 주세요. 서버가 안내한 대기 시간이 지나기 전에는 같은 발송을 반복하지 마세요. + + + +## `bound_courier_marker_invalid` + +The bound courier marker is invalid. + +요청의 인증 메시지 연결 표식이 유효하지 않습니다. + +표식을 직접 수정하지 마세요. 공식 로그인 화면에서 인증 요청을 다시 시작해 주세요. + + + +## `bound_courier_recipient_mismatch` + +The bound courier recipient does not match. + +인증 메시지의 수신 대상이 해당 로그인 요청에 연결된 대상과 다릅니다. + +로그인 요청에 등록한 수신 대상과 현재 발송 대상을 비교해 주세요. 올바른 수신 대상으로 새 로그인 절차를 시작해 주세요. + + + +## `bound_courier_replay_mismatch` + +The bound courier replay does not match. + +재전송 요청의 인증 코드가 기존 로그인 시도에 저장된 코드와 다릅니다. + +기존 요청의 처리 상태를 먼저 확인해 주세요. 서로 다른 로그인 시도의 코드와 요청 식별자를 섞지 마세요. + + + +## `bound_courier_state_invalid` + +The bound courier state is invalid. + +인증 메시지 요청의 저장 상태가 없거나 현재 처리 단계와 맞지 않습니다. + +기존 요청이 완료되거나 만료되었는지 확인해 주세요. 상태가 불명확하면 요청 식별자로 담당자에게 확인을 요청해 주세요. + + + +## `bound_courier_state_unavailable` + +The bound courier state is unavailable. + +인증 메시지 처리에 필요한 서버 상태를 읽거나 저장하지 못했습니다. + +메시지 수신 여부와 로그인 진행 상태를 먼저 확인해 주세요. 상태 저장소가 복구되기 전에는 발송 요청을 반복하지 마세요. + + + +## `bound_courier_template_invalid` + +The bound courier template is invalid. + +발송 요청의 메시지 템플릿이 해당 인증 절차에서 허용되지 않습니다. + +발송 템플릿과 로그인 절차의 종류를 비교해 주세요. 서비스 관리자에게 허용된 템플릿 구성 확인을 요청해 주세요. + + + +## `bound_courier_unavailable` + +The bound courier service is unavailable. + +로그인 요청에 연결된 인증 메시지 서비스를 사용할 수 없습니다. + +서비스 관리자에게 메시지 서비스 구성과 연결 상태 확인을 요청해 주세요. 이미 발송 요청을 했다면 수신 여부도 먼저 확인해 주세요. + + + +## `client_disabled` + +The client is disabled. + +요청한 애플리케이션이 비활성 상태입니다. + +사용할 애플리케이션 식별자가 맞는지 확인해 주세요. 사용 재개가 필요하면, 서비스 관리자에게 검토를 요청해 주세요. + + + +## `client_forbidden` + +The client is not allowed to perform this operation. + +해당 애플리케이션에 현재 작업이 허용되지 않았습니다. + +애플리케이션 식별자와 요청 작업을 확인해 주세요. 서비스 관리자에게 허용된 기능 범위의 검토를 요청해 주세요. + + + +## `client_log_body_too_large` + +The client log body exceeds the permitted size. + +클라이언트 로그 요청의 본문이 허용된 크기를 초과했습니다. + +로그 항목 수와 각 항목의 길이를 줄여 주세요. API가 허용한 크기로 나누어 전송해 주세요. + + + +## `client_log_content_type_unsupported` + +The client log content type is not supported. + +클라이언트 로그 요청의 Content-Type을 서버가 지원하지 않습니다. + +로그 API가 요구하는 Content-Type과 본문 형식을 사용해 주세요. + + + +## `client_log_origin_forbidden` + +The client log origin is not allowed. + +클라이언트 로그 요청의 출처가 허용 목록과 맞지 않습니다. + +실제 서비스 주소와 허용된 출처 구성을 비교해 주세요. 관리자에게 등록 상태 확인을 요청해 주세요. + + + +## `client_log_payload_invalid` + +The client log payload is invalid. + +클라이언트 로그의 필드나 값이 수집 형식에 맞지 않습니다. + +오류로 표시된 로그 필드를 API 형식과 비교해 주세요. 지원되지 않는 값이나 구조를 수정해 주세요. + + + +## `client_log_rate_limited` + +Client log requests exceed the permitted rate. + +클라이언트 로그 전송 빈도가 허용량을 초과했습니다. + +Retry-After가 있으면, 해당 시간 동안 기다려 주세요. 중복 로그를 줄이고 전송 간격을 늘려 주세요. + + + +## `client_profile_required` + +The client profile is required for this operation. + +서비스 이용 조건을 판단할 애플리케이션 프로필이 없습니다. + +서비스 관리자에게 해당 애플리케이션의 프로필 등록 상태를 확인해 주세요. 프로필 문제가 해결되면 동의 절차를 다시 진행해 주세요. + + + +## `conflict` + +The request conflicts with the current resource state. + +요청이 현재 자원의 상태와 충돌합니다. + +자원의 최신 상태를 조회해 주세요. 변경할 내용을 다시 검토한 뒤 현재 상태에 맞는 요청을 작성해 주세요. + + + +## `consent_binding_changed` + +Consent data changed during approval. + +승인 처리 중 동의에 연결된 정보가 변경되었습니다. + +기존 승인 요청을 재전송하지 마세요. 로그인부터 다시 시작하고 최신 동의 내용을 검토해 주세요. + + + +## `consent_binding_persist_failed` + +The service failed to save consent data. + +서버가 승인에 필요한 동의 연결 정보를 저장하지 못했습니다. + +같은 승인 요청을 재전송하지 마세요. 저장소 상태가 복구되면, 로그인부터 다시 시작해 주세요. + + + +## `consent_binding_unavailable` + +Consent binding data is unavailable. + +동의를 사용자와 로그인 요청에 연결하는 데이터를 사용할 수 없습니다. + +담당자에게 동의 연결 데이터 상태를 확인해 주세요. 응답의 다음 조치에 따라 로그인 절차를 진행해 주세요. + + + +## `consent_binding_verify_unavailable` + +Consent data verification is unavailable. + +서버가 승인 후 동의 연결 정보의 일치를 확인하지 못했습니다. + +이미 사용한 승인 요청을 다시 보내지 마세요. 요청 식별자로 처리 상태를 확인한 뒤 로그인 절차를 새로 시작해 주세요. + + + +## `consent_client_mismatch` + +The consent client does not match the request. + +동의 요청의 애플리케이션이 현재 로그인에 연결된 애플리케이션과 다릅니다. + +처음 접속한 서비스 주소와 애플리케이션 식별자를 확인해 주세요. 올바른 서비스에서 로그인을 다시 시작해 주세요. + + + +## `consent_contract_changed` + +The consent contract changed before approval. + +승인에 필요한 동의 조건이 기존에 동의한 조건과 달라졌습니다. + +최신 동의 화면에서 변경된 내용을 검토해 주세요. 필요한 동의를 완료한 뒤 요청을 새로 시작해 주세요. + + + +## `consent_login_binding_missing` + +The consent login binding is missing. + +동의 요청에 필요한 로그인 연결 정보가 없습니다. + +서비스에서 로그인 절차를 처음부터 시작해 주세요. 기존 동의 주소를 별도 창에 복사해 사용하지 마세요. + + + +## `consent_login_mismatch` + +The consent login does not match the request. + +동의 요청에 연결된 로그인과 현재 로그인 정보가 다릅니다. + +사용 중인 계정과 로그인 창을 확인해 주세요. 같은 서비스와 계정으로 로그인 절차를 다시 시작해 주세요. + + + +## `consent_missing_or_revoked` + +Required consent is missing or revoked. + +요청한 서비스를 이용할 동의가 없거나 철회되었습니다. + +서비스가 제공하는 동의 절차를 시작해 주세요. 요청한 범위를 검토한 뒤 필요한 동의를 완료해 주세요. + + + +## `consent_policy_denied` + +The sign in policy denied consent approval. + +현재 로그인 정책이 동의 승인을 허용하지 않습니다. + +서비스 관리자에게 거절된 정책과 사용 자격을 확인해 주세요. 정책을 충족하기 전에는 같은 승인을 반복하지 마세요. + + + +## `consent_policy_unavailable` + +The sign in policy is unavailable. + +서버가 동의 승인에 필요한 로그인 정책을 확인하지 못했습니다. + +정책 조회 서비스가 복구되었는지 담당자에게 확인해 주세요. 복구 후 로그인부터 다시 시작해 주세요. + + + +## `consent_request_invalid` + +The consent request is invalid or expired. + +동의 승인 요청이 유효하지 않거나 만료되었습니다. + +기존 승인 요청을 재전송하지 마세요. 서비스의 로그인 시작 화면에서 새 요청을 생성해 주세요. + + + +## `consent_required` + +Required user consent is missing or revoked. + +서비스 이용에 필요한 사용자 동의가 없거나 철회되었습니다. + +공식 동의 화면에서 요청한 범위를 검토해 주세요. 필요한 동의를 완료한 뒤 서비스를 이용해 주세요. + + + +## `consent_session_invalid` + +The consent session is invalid. + +동의 승인에 사용한 세션이 유효하지 않습니다. + +현재 서비스에서 다시 로그인해 주세요. 새 로그인에 연결된 동의 화면에서 승인해 주세요. + + + +## `consent_upstream_failed` + +The authentication service failed to approve consent. + +연결된 인증 서비스가 동의 승인을 완료하지 못했습니다. + +요청 식별자로 승인 처리 상태를 먼저 확인해 주세요. 이미 사용한 승인 요청 대신 새 로그인 절차를 시작해 주세요. + + + +## `consent_upstream_rate_limited` + +The authentication service rate limited consent approval. + +인증 서비스가 동의 승인 요청의 빈도를 제한했습니다. + +Retry-After가 있으면, 안내한 시간 동안 기다려 주세요. 대기 후 로그인부터 새로 시작해 주세요. + + + +## `consent_upstream_response_invalid` + +The authentication service returned an invalid consent response. + +인증 서비스의 동의 승인 응답이 예상한 형식이나 조건에 맞지 않습니다. + +승인 요청을 반복하기 전에 요청 식별자로 처리 결과를 확인해 주세요. 담당자의 확인 후 로그인부터 다시 시작해 주세요. + + + +## `consent_usage_record_failed` + +The service failed to record consent usage. + +승인 과정에서 동의 사용 기록을 저장하지 못했습니다. + +승인 상태가 불명확하므로 요청 식별자로 처리 결과를 먼저 확인해 주세요. 기존 승인 요청을 재전송하지 마세요. + + + +## `creation_audit_pending` + +User creation committed; audit delivery is pending. Retry with the same Idempotency-Key. + +사용자는 생성됐지만 감사 저장소에 생성 기록을 전달하지 못했습니다. 서버가 원장의 최초 수행자와 대상 UUID를 유지하여 전달을 재시도합니다. + +기존 Idempotency-Key로 재시도해 주세요. 새 키로 같은 사용자를 생성하지 마세요. 문제가 계속되면 서비스 관리자에게 감사 저장소 상태 확인을 요청해 주세요. + + + +## `credential_conflict` + +The credential conflicts with an existing credential. + +연동 자격 증명과 대화형 로그인 자격 증명이 한 요청에 함께 포함되어 있습니다. + +호출할 API가 요구하는 인증 방식 하나만 사용해 주세요. 연동 API에는 브라우저 세션이나 대화형 인증 정보를 함께 보내지 마세요. + + + +## `credential_create_failed` + +The service failed to create the credential. + +서버가 연동 자격 증명 생성 요청을 완료하지 못했습니다. + +자격 증명 목록에서 생성 결과를 먼저 확인해 주세요. 결과가 불명확하면 요청 식별자로 관리자에게 문의해 주세요. + + + +## `credential_exposure_exceeded` + +The credential does not allow the requested exposure. + +요청한 정보의 공개 수준이 자격 증명에 허용된 수준을 초과했습니다. + +요청의 exposure와 자격 증명의 허용 범위를 비교해 주세요. 필요한 정보 접근은 관리자에게 별도 승인을 요청해 주세요. + + + +## `credential_generation_failed` + +The service failed to generate the credential. + +서버가 자격 증명의 비밀값을 생성하거나 안전하게 저장할 형태로 처리하지 못했습니다. + +서비스 관리자에게 자격 증명 생성 기능 확인을 요청해 주세요. 임의로 비밀값을 만들어 등록하지 마세요. + + + +## `credential_list_failed` + +The service failed to list credentials. + +서버가 자격 증명 목록을 조회하지 못했습니다. 목록이 비었다는 뜻은 아닙니다. + +관리자에게 저장소 상태를 확인해 주세요. 서비스가 복구되면, 목록을 다시 조회해 주세요. + + + +## `credential_not_active` + +The credential is not active. + +작업 대상 자격 증명이 활성 상태가 아닙니다. + +자격 증명의 최신 상태를 조회해 주세요. 비활성 자격 증명에 허용된 작업인지 확인해 주세요. + + + +## `credential_not_found` + +The requested credential does not exist. + +요청한 식별자에 해당하는 자격 증명을 찾지 못했습니다. + +자격 증명 목록에서 대상 식별자를 확인해 주세요. 삭제된 대상이면, 그 식별자로 변경 요청을 보내지 마세요. + + + +## `credential_not_inactive` + +The credential is not inactive. + +비활성 이력에만 허용된 재발급을 다른 상태의 자격 증명에 요청했습니다. + +자격 증명의 최신 상태와 재발급 조건을 확인해 주세요. 활성 자격 증명을 임의로 비활성화하지 마세요. + + + +## `credential_operation_mismatch` + +The credential does not grant the requested operation. + +자격 증명에 연결된 OperationID가 요청한 작업과 다릅니다. + +요청 경로와 자격 증명의 OperationID를 비교해 주세요. 담당 관리자에게 필요한 작업 전용 자격 증명을 요청해 주세요. + + + +## `credential_operation_not_available` + +The requested credential operation is unavailable. + +요청한 작업이 연동 자격 증명으로 사용할 수 있는 작업 목록에 없습니다. + +공개 연동 API의 지원 작업 목록을 확인해 주세요. 지원되지 않는 관리자 API를 연동 키로 호출하지 마세요. + + + +## `credential_scope_exceeded` + +The requested scope exceeds the credential scope. + +대상 Tenant가 연동 자격 증명에 허용된 범위를 벗어났습니다. + +대상 Tenant와 자격 증명의 scope를 비교해 주세요. 다른 범위 접근이 필요하면, 관리자에게 검토를 요청해 주세요. + + + +## `credential_store_unavailable` + +The credential store is unavailable. + +연동 자격 증명을 조회하는 저장소를 사용할 수 없습니다. 키의 유효성을 판정한 결과가 아닙니다. + +저장소 연결 상태를 담당자에게 확인해 주세요. 이 오류만으로 기존 키를 교체하지 마세요. + + + +## `credential_update_failed` + +The service failed to update the credential. + +자격 증명 변경 요청의 저장을 완료하지 못했습니다. + +목록에서 대상 자격 증명의 현재 상태를 먼저 확인해 주세요. 변경 결과가 불명확하면 요청 식별자로 문의해 주세요. + + + +## `credential_usage_record_failed` + +The service failed to record credential usage. + +서버가 연동 자격 증명 사용 기록을 남기지 못했습니다. + +감사 기록 저장소 상태를 담당자에게 확인해 주세요. 변경 요청이면, 실제 자원 상태를 확인한 뒤 후속 조치를 결정해 주세요. + + + +## `cursor_expired` + +The cursor expired. + +다음 페이지를 조회하는 cursor의 유효 시간이 지났습니다. + +기존 cursor를 제거하고 첫 페이지부터 다시 조회해 주세요. + + + +## `cursor_invalid` + +The cursor is invalid. + +다음 페이지 조회용 cursor가 유효한 서버 발급 값이 아닙니다. + +cursor를 직접 수정하지 마세요. 첫 페이지 응답이 제공한 값을 사용해 주세요. + + + +## `cursor_mismatch` + +The cursor does not match the request. + +cursor가 현재 요청에 필요한 연결 조건과 맞지 않습니다. + +처음 조회한 요청 조건을 확인해 주세요. 조건을 바꿨으면, cursor 없이 첫 페이지부터 조회해 주세요. + + + +## `cursor_query_mismatch` + +The cursor query does not match the request. + +cursor를 발급할 때 사용한 검색 조건과 현재 조건이 다릅니다. + +기존 검색 조건을 유지하거나 cursor 없이 변경한 조건으로 첫 페이지를 조회해 주세요. + + + +## `cursor_scope_mismatch` + +The cursor scope does not match the request. + +cursor를 발급한 권한 범위와 현재 조회 범위가 다릅니다. + +현재 허용된 Tenant 범위를 확인해 주세요. 다른 범위의 cursor를 재사용하지 말고 첫 페이지부터 조회해 주세요. + + + +## `cursor_unavailable` + +The cursor service is unavailable. + +서버가 페이지 이동 정보를 처리하는 기능을 사용할 수 없습니다. + +조회 서비스의 복구 여부를 담당자에게 확인해 주세요. 복구되면, 첫 페이지부터 조회를 시작해 주세요. + + + +## `dependency_unavailable` + +A required dependency is unavailable. + +요청 처리에 필요한 연결 서비스나 저장소를 사용할 수 없습니다. + +응답의 상세 정보로 영향을 받은 기능을 확인해 주세요. 변경 요청이면, 기존 작업의 처리 상태를 먼저 조회해 주세요. + + + +## `device_key_conflict` + +The device key conflicts with an existing device registration. + +기기 공개 키가 기존 기기 등록과 충돌합니다. + +기기 등록 목록에서 같은 키의 등록 상태를 확인해 주세요. 새 등록을 반복하기 전에 기존 기기를 복구할지 담당자와 결정해 주세요. + + + +## `device_not_found` + +The requested device does not exist. + +요청한 식별자에 해당하는 기기를 찾지 못했습니다. + +현재 계정의 기기 목록에서 식별자를 확인해 주세요. 등록이 필요한 기기이면, 공식 등록 절차를 진행해 주세요. + + + +## `device_proof_expired` + +The device proof expired. + +서버가 기기 증명의 만료 시각이 지났다고 판정했습니다. 이 판정만으로 기기 시계 오류를 확정할 수 없습니다. + +기기와 서버의 시간 동기화 상태를 확인해 주세요. 만료된 증명 대신 현재 요청에 맞는 새 증명을 생성해 주세요. + + + +## `device_proof_issued_at_future` + +The device proof issue time is beyond the permitted future range. + +기기 증명의 발급 시각이 서버 기준 허용된 미래 범위를 초과했습니다. 기기 시계가 원인인지는 추가 확인이 필요합니다. + +기기와 서버의 시간 및 증명의 iat 값을 비교해 주세요. 시간 생성 로직을 확인한 뒤 새 증명을 생성해 주세요. + + + +## `device_proof_issued_at_too_old` + +The device proof issue time is beyond the permitted age. + +기기 증명의 발급 시각이 서버가 허용하는 기간보다 오래되었습니다. + +저장된 증명을 재사용하는지 확인해 주세요. 기기와 서버의 시간 상태를 확인하고 현재 요청용 증명을 생성해 주세요. + + + +## `device_proof_not_yet_valid` + +The device proof is not valid yet. + +기기 증명의 사용 시작 시각이 서버 기준 현재 시각보다 늦습니다. + +증명의 nbf 값과 서버 시간을 비교해 주세요. 기기 시계와 증명 생성 로직을 확인해 주세요. + + + +## `device_proof_time_claims_invalid` + +The device proof time claims are missing or invalid. + +기기 증명의 시간 필드가 없거나 허용된 유효 시간 조건에 맞지 않습니다. + +증명의 iat와 exp 값 및 유효 시간 길이를 API 계약과 비교해 주세요. 기기 시계만 원인으로 단정하지 마세요. + + + +## `device_state_conflict` + +The device is in a state that conflicts with the requested operation. + +기기의 현재 상태에서 요청한 작업을 수행할 수 없습니다. + +기기 목록에서 최신 상태를 확인해 주세요. 해당 상태에 허용된 작업을 선택해 주세요. + + + +## `device_version_conflict` + +The device version does not match the expected version. + +요청에 지정한 기기 버전이 서버의 최신 버전과 다릅니다. + +기기 정보를 다시 조회해 주세요. 최신 버전에 대한 변경 내용을 검토한 뒤 제출해 주세요. + + + +## `diagnostic_already_running` + +A diagnostic job is already running on this instance. + +같은 인스턴스에서 진단 작업이 이미 실행 중입니다. + +기존 작업이 끝나거나 취소된 뒤 새 작업을 시작해 주세요. + + + +## `diagnostic_capture_active` + +Another diagnostic capture is active on this instance. + +같은 인스턴스에서 스택 또는 진단 수집이 진행 중입니다. + +진행 중인 수집이 끝난 뒤 다시 시도해 주세요. + + + +## `diagnostics_disabled` + +Performance diagnostics are disabled on this instance. + +현재 인스턴스의 진단 실행이 허용되지 않았습니다. + +비운영 환경의 진단용 빌드와 실행 플래그를 확인해 주세요. Production에서는 실행할 수 없습니다. + + + +## `directory_global_scope_forbidden` + +Directory operations require a tenant scope. + +Directory 작업에 전체 범위를 지정했지만 이 작업은 Tenant 범위만 허용합니다. + +구체적인 대상 Tenant를 지정해 주세요. 그 Tenant에 허용된 연동 자격 증명을 사용해 주세요. + + + +## `directory_integration_unavailable` + +The directory integration is unavailable. + +Directory 연동 처리에 필요한 기능을 사용할 수 없습니다. + +연동 서비스와 연결 상태를 담당자에게 확인해 주세요. 변경 요청이면, 대상 사용자와 소속의 현재 상태를 먼저 조회해 주세요. + + + +## `directory_mutation_failed` + +The directory operation failed. + +Directory 사용자 또는 소속 변경을 완료하지 못했습니다. + +대상 사용자의 현재 상태와 작업 기록을 먼저 확인해 주세요. 결과가 불명확하면 새 요청을 만들기 전에 요청 식별자로 문의해 주세요. + + + +## `directory_scope_required` + +A tenant scope is required for this directory operation. + +Directory 작업에 필요한 tenant\_subtree 범위가 지정되지 않았습니다. + +연동 자격 증명의 scope.kind와 scope.tenantId를 확인해 주세요. 담당 관리자에게 대상 Tenant로 한정된 자격 증명을 요청해 주세요. + + + +## `expected_revision_required` + +The current profile revision is required. + +수정 요청에 현재 revision이 없습니다. + +프로필 조회 응답의 revision을 expectedRevision으로 전달해 주세요. + + + +## `expired_token` + +The token expired. + +요청에 사용한 토큰 또는 인증 코드의 유효 시간이 지났습니다. + +만료된 값을 재사용하지 마세요. 해당 로그인 또는 인증 절차에서 새 값을 발급받아 주세요. + + + +## `export_too_large` + +The export exceeds the permitted size. + +내보내기 대상이 허용된 크기나 건수를 초과했습니다. + +조회 기간과 대상 범위를 줄여 주세요. 필요한 결과를 허용된 범위로 나누어 내보내 주세요. + + + +## `forbidden` + +The authenticated principal is not allowed to perform this operation. + +인증한 주체에 요청 작업을 수행할 권한이 없습니다. + +대상 자원과 현재 권한 범위를 확인해 주세요. 업무상 필요한 권한은 승인 절차로 요청해 주세요. + + + +## `fresh_totp_required` + +A fresh TOTP authentication is required. + +현재 작업에 사용할 만큼 최근에 수행한 TOTP 인증 기록이 없습니다. + +로그인 화면에서 시간 기반 일회용 코드인 TOTP로 다시 인증해 주세요. 인증 후 원래 작업으로 돌아가 주세요. + + + +## `gateway_timeout` + +The upstream service did not respond in time. + +연결된 서비스가 제한 시간 안에 응답하지 않았습니다. 작업 미실행이나 실패가 확정된 상태는 아닙니다. + +기존 작업 상태와 대상 자원을 먼저 조회해 주세요. 재전송이 필요하면, API의 중복 방지 규칙과 기존 Idempotency-Key를 유지해 주세요. + + + +## `hanmac_email_local_part_conflict` + +The Hanmac email local part conflicts with another account. + +Hanmac 이메일에서 @ 앞부분이 다른 계정과 충돌합니다. + +해당 이메일 식별자를 사용하는 기존 계정을 확인해 주세요. 다른 계정의 식별자를 덮어쓰지 말고 관리자에게 조정을 요청해 주세요. + + + +## `headless_jwks_refresh_failed` + +The headless login key set refresh failed. + +화면 없는 로그인에 사용하는 공개 키 집합을 갱신하지 못했습니다. + +서비스 관리자에게 등록한 JWKS 주소의 응답과 키 게시 상태를 확인해 주세요. 키 검증을 생략하지 마세요. + + + +## `hr_integration_unavailable` + +HR integration is unavailable. + +HR 연동에 필요한 서비스가 응답하지 않습니다. + +잠시 후 같은 Idempotency-Key로 다시 요청해 주세요. + + + +## `hr_tenant_scope_required` + +HR operations require a hanmac-family tenant subtree. + +HR 키의 조직 범위가 hanmac-family 하위 조직이 아닙니다. + +hanmac-family 하위의 구체적인 조직을 선택해 키를 발급해 주세요. + + + +## `id_token_claims_invalid` + +The identity token claims are invalid. + +ID token에 넣을 사용자 정보 필드의 구성이 허용된 조건에 맞지 않습니다. + +metadata.id\_token\_claims.top\_level 값과 허용된 필드 구성을 확인해 주세요. 서비스 관리자에게 토큰 정보 구성의 수정을 요청해 주세요. + + + +## `idempotency_conflict` + +The idempotency key is already bound to a different request. + +같은 Idempotency-Key가 다른 요청 내용에 이미 연결되어 있습니다. + +해당 키로 처음 보낸 요청과 처리 상태를 먼저 확인해 주세요. 재시도이면, 처음 보낸 본문과 같은 키를 유지해 주세요. 독립된 새 작업일 때만 새 키를 사용해 주세요. + + + +## `idempotency_key_required` + +An idempotency key is required for this operation. + +중복 실행을 구분할 Idempotency-Key가 요청에 없습니다. + +하나의 작업에 사용할 고유한 Idempotency-Key를 지정해 주세요. 같은 작업을 재전송할 때는 같은 키와 본문을 유지해 주세요. + + + +## `identifier_conflict` + +A login identifier is already in use. + +전화번호 등 로그인 식별자를 다른 사용자가 사용하고 있습니다. + +다른 사용자의 식별자와 겹치지 않는 값으로 수정해 주세요. + + + +## `identity_lookup_failed` + +The service failed to look up the identity. + +서버가 사용자 원장 조회를 완료하지 못했습니다. 사용자가 없다는 판정은 아닙니다. + +사용자 식별자와 요청 식별자를 기록해 주세요. 담당자에게 원장 조회 상태를 확인한 뒤 조회를 진행해 주세요. + + + +## `identity_lookup_unavailable` + +The identity lookup service is unavailable. + +사용자 원장을 조회하는 서비스를 사용할 수 없습니다. + +원장 서비스의 연결 상태를 담당자에게 확인해 주세요. 조회 실패를 근거로 사용자를 새로 만들거나 삭제하지 마세요. + + + +## `identity_mirror_unavailable` + +The identity mirror is unavailable. + +사용자 원장을 복제한 조회 데이터를 사용할 수 없습니다. + +담당자에게 복제 데이터의 저장소와 동기화 상태를 확인해 주세요. 사용자 부재로 판단하지 마세요. + + + +## `identity_visibility_unavailable` + +Identity visibility data is unavailable. + +사용자 노출 범위를 판단하는 데이터를 사용할 수 없습니다. + +담당자에게 노출 정책 조회 상태를 확인해 주세요. 정책 확인 없이 사용자 정보를 다른 경로로 조회하지 마세요. + + + +## `import_preview_changed` + +The import preview changed before submission. + +가져오기 미리보기와 제출 시점의 대상 데이터가 달라졌습니다. + +가져오기 미리보기를 새로 생성해 주세요. 변경된 대상과 결과를 검토한 뒤 제출해 주세요. + + + +## `incomparable_performance_runs` + +The measurements do not satisfy the performance comparison requirements. + +측정 조건이 다르거나 정상 실행의 표본 조건을 만족하지 않습니다. + +환경, fixture, 동시성, 측정 항목, 워밍업, 오류 수와 진단 플래그를 확인해 주세요. + + + +## `inline_headless_jwks_not_allowed` + +Inline headless login keys are not allowed. + +요청 본문에 직접 포함한 headless 로그인 공개 키를 허용하지 않습니다. + +API가 요구하는 공개 키 등록 방식을 확인해 주세요. 서비스 관리자에게 허용된 키 배포 경로 구성을 요청해 주세요. + + + +## `installed_refresh_policy_not_allowed` + +The installed refresh policy does not allow this operation. + +애플리케이션 유형이 요청한 설치형 앱 갱신 정책을 허용하지 않습니다. + +applicationProfile과 installedAppRefreshPolicy의 허용 조합을 확인해 주세요. 서버 간 통신용 애플리케이션에 설치형 앱 정책을 지정하지 마세요. + + + +## `integration_credential_expired` + +The integration credential expired. + +연동 자격 증명의 만료 시간이 지났습니다. + +관리자에게 승인된 갱신 또는 재발급 절차를 요청해 주세요. 새 자격 증명을 적용하기 전에 사용 범위를 확인해 주세요. + + + +## `integration_credential_not_allowed` + +The integration credential is not allowed. + +현재 경로는 연동 자격 증명을 인증 수단으로 허용하지 않습니다. + +연동 작업에는 공개된 /integrations/\* 경로를 사용해 주세요. 관리자 작업은 권한이 있는 사용자의 대화형 인증으로 진행해 주세요. + + + +## `integration_credential_required` + +An integration credential is required. + +연동 API에 필요한 키 식별자나 비밀값이 없습니다. + +X-Baron-Key-ID와 X-Baron-Key-Secret 헤더의 전달 여부를 확인해 주세요. 비밀값을 로그나 오류 보고에 넣지 마세요. + + + +## `interactive_client_credentials_not_allowed` + +Interactive clients cannot use client credentials. + +대화형 애플리케이션이 client\_credentials 방식을 요청했습니다. + +애플리케이션의 실제 사용 목적과 grantTypes를 확인해 주세요. 서버 간 통신이 필요하면, 별도 m2m 애플리케이션 등록을 담당자에게 요청해 주세요. + + + +## `internal_domain_personal_assignment_rejected` + +Internal email domains cannot be assigned to personal tenants. + +내부 이메일 도메인 사용자를 Personal 테넌트에 소속시키려 했습니다. + +해당 사용자의 대표 소속을 회사 또는 조직 테넌트로 지정해 주세요. + + + +## `internal_error` + +The server failed to complete the request. + +서버 내부 오류로 요청 처리를 완료하지 못했습니다. + +변경 요청이면, 자원의 현재 상태를 먼저 확인해 주세요. 요청 식별자와 발생 시각을 서비스 관리자에게 전달해 주세요. + + + +## `invalid_client` + +The client authentication is invalid. + +애플리케이션 인증 정보가 유효하지 않습니다. + +client\_id와 해당 애플리케이션에 등록된 인증 방식을 확인해 주세요. 비밀값이나 서명 키의 등록 상태는 관리자에게 확인해 주세요. + + + +## `invalid_client_assertion_audience` + +The client assertion audience does not match. + +클라이언트가 서명한 인증 문서의 aud 값이 서버가 기대한 대상과 다릅니다. + +등록된 토큰 발급 주소와 aud 값을 비교해 주세요. 올바른 대상을 넣어 인증 문서를 다시 생성해 주세요. + + + +## `invalid_client_assertion_expired` + +The client assertion expiry is not valid. + +클라이언트 인증 문서의 만료 조건이 유효하지 않습니다. + +인증 문서의 exp 값과 서버 시간을 비교해 주세요. 허용된 유효 시간으로 새 인증 문서를 생성해 주세요. + + + +## `invalid_client_assertion_iat_future` + +The client assertion issue time is not valid. + +클라이언트 인증 문서의 발급 시각이 서버의 허용 범위를 벗어났습니다. + +iat 값과 클라이언트 및 서버의 시간을 비교해 주세요. 시간 동기화와 발급 로직을 확인해 주세요. + + + +## `invalid_client_assertion_iss_sub` + +The client assertion issuer or subject does not match. + +클라이언트 인증 문서의 iss 또는 sub 값이 등록한 클라이언트와 맞지 않습니다. + +등록된 client\_id와 iss 및 sub 값을 비교해 주세요. 다른 애플리케이션의 인증 문서를 사용하지 마세요. + + + +## `invalid_client_assertion_jwks_load` + +The client assertion keys are unavailable. + +클라이언트 인증 문서를 검증할 공개 키를 읽지 못했습니다. + +등록된 JWKS 주소의 접근 가능 여부와 응답 형식을 확인해 주세요. 키를 읽을 수 있을 때까지 인증 검증을 우회하지 마세요. + + + +## `invalid_client_assertion_not_before` + +The client assertion is not active yet. + +클라이언트 인증 문서를 사용할 수 있는 시작 시각이 아직 되지 않았습니다. + +nbf 값과 서버 시간을 비교해 주세요. 클라이언트의 시간 상태와 인증 문서 생성 로직을 확인해 주세요. + + + +## `invalid_client_assertion_parse` + +The client assertion format is invalid. + +클라이언트 인증 문서의 형식을 해석할 수 없습니다. + +API가 요구하는 서명 문서 형식과 인코딩을 확인해 주세요. 전송 과정에서 값이 잘리거나 변형되었는지 확인해 주세요. + + + +## `invalid_client_assertion_signature` + +The client assertion signature is invalid. + +클라이언트 인증 문서의 서명을 등록된 공개 키로 검증하지 못했습니다. + +서명 키와 공개 키의 짝 및 kid 값을 확인해 주세요. 키 교체 중이면, 공개 키 게시가 끝났는지 확인해 주세요. + + + +## `invalid_code` + +The supplied authorization code is invalid. + +제출한 인증 또는 인가 코드가 현재 요청에서 유효하지 않습니다. + +코드를 발급한 로그인 요청과 현재 요청이 같은지 확인해 주세요. 사용했거나 만료된 코드이면, 공식 절차에서 새 코드를 발급받아 주세요. + + + +## `invalid_device_proof` + +The device proof is invalid. + +기기 서명이나 요청에 결합된 값이 검증을 통과하지 못했습니다. + +기기 키, audience, 요청 hash, 발급 시각과 jti를 확인해 주세요. 같은 proof를 다시 전송하지 마세요. + + + +## `invalid_email` + +The email address is invalid. + +이메일 값이 허용된 주소 형식에 맞지 않습니다. + +이메일 주소의 형식과 입력 오류를 확인해 주세요. 오류가 표시된 필드만 수정해 주세요. + + + +## `invalid_employee_number` + +The employee number must be a string of at most 20 characters or null. + +사번의 형식 또는 길이가 올바르지 않습니다. + +사번은 최대 20자의 문자열로 전달해 주세요. 삭제할 때는 null 또는 빈 문자열을 사용해 주세요. + + + +## `invalid_expiry` + +The expiry value is invalid. + +요청한 만료 값이 허용된 기간이나 형식에 맞지 않습니다. + +해당 API의 만료 필드 형식과 허용 범위를 확인해 주세요. 그 범위에 맞는 값을 제출해 주세요. + + + +## `invalid_exposure` + +The exposure value is invalid. + +정보 공개 수준으로 허용되지 않은 값을 요청했습니다. + +exposure에 허용된 structure, members, identifiers 중 필요한 값을 사용해 주세요. 자격 증명이 그 수준을 허용하는지도 확인해 주세요. + + + +## `invalid_format` + +The supplied value has an invalid format. + +입력 값이 해당 필드에 필요한 형식과 맞지 않습니다. + +응답에 표시한 필드와 요구 형식을 확인해 주세요. 값을 해당 형식으로 수정해 주세요. + + + +## `invalid_grant` + +The supplied grant is invalid or unusable. + +제출한 인가 코드나 갱신 수단을 현재 요청에 사용할 수 없습니다. + +해당 값의 만료와 사용 이력 및 클라이언트 연결을 확인해 주세요. 로그인 재시작이 필요한 경우 공식 로그인 절차를 따라 주세요. + + + +## `invalid_integration_credential` + +The integration credential is invalid or inactive. + +연동 자격 증명이 유효하지 않거나 활성 상태가 아닙니다. + +키 식별자와 비밀값의 짝을 확인해 주세요. 관리자에게 폐기·교체·만료 상태를 확인해 주세요. + + + +## `invalid_job_request` + +The operations job request is invalid. + +지원하지 않는 작업 종류나 진단 옵션입니다. + +작업 종류와 CPU 수집 시간 1~30초 조건을 확인해 주세요. + + + +## `invalid_json` + +The request body contains invalid JSON. + +요청 본문을 올바른 JSON으로 해석할 수 없습니다. + +따옴표와 쉼표 등 JSON 문법을 확인해 주세요. 올바른 JSON 본문과 Content-Type을 전송해 주세요. + + + +## `invalid_login_identifier` + +The employee number does not satisfy the login identifier policy. + +사번으로 만든 로그인 식별자가 정책을 만족하지 않습니다. + +해당 조직의 로그인 식별자 정책에 맞는 사번을 전달해 주세요. + + + +## `invalid_login_prepare_request` + +The login preparation request is invalid. + +로그인 준비 요청이 필요한 입력 조건에 맞지 않습니다. + +공식 로그인 시작 화면에서 요청을 생성해 주세요. 연동 구현이면, 필수 필드와 허용된 요청 형식을 확인해 주세요. + + + +## `invalid_membership_delta` + +The membership change is invalid. + +추가하거나 제거할 소속의 변경 내용이 유효하지 않습니다. + +추가 목록과 제거 목록의 중복 및 대상 Tenant를 확인해 주세요. 현재 소속을 기준으로 변경 요청을 다시 작성해 주세요. + + + +## `invalid_name` + +The name must contain at most 100 characters. + +이름이 비어 있거나 허용 길이를 초과했습니다. + +제어문자 없이 1~100자의 이름을 전달해 주세요. + + + +## `invalid_operation_id` + +The operation ID must be a UUID. + +작업 식별자가 UUID 형식이 아닙니다. + +수정 응답의 statusUrl로 작업 상태를 조회해 주세요. + + + +## `invalid_operation_payload` + +The operation payload is invalid. + +작업 본문이 해당 작업에 요구되는 구조나 조건과 맞지 않습니다. + +선택한 OperationID의 요청 형식과 본문을 비교해 주세요. 필요한 필드와 값만 수정해 주세요. + + + +## `invalid_operations_queue` + +The operations queue is not supported. + +요청한 큐 종류를 지원하지 않습니다. + +queue 경로에는 keto 또는 worksmobile을 사용하세요. + + + +## `invalid_or_expired_code` + +The supplied code is invalid or expired. + +제출한 코드가 유효하지 않거나 만료되었습니다. + +현재 로그인 요청에서 받은 최신 코드를 확인해 주세요. 만료된 코드이면, 공식 인증 절차에서 새 코드를 요청해 주세요. + + + +## `invalid_performance_import` + +The performance import has an invalid JSON structure. + +성능 비교 JSON 형식 또는 필드가 유효하지 않습니다. + +운영 센터 사용법의 입력 필드와 JSON 구문을 확인해 주세요. + + + +## `invalid_phone` + +The phone number is invalid. + +전화번호 값이 해당 필드의 입력 조건에 맞지 않습니다. + +국가 코드와 번호 형식을 확인해 주세요. API가 요구한 형식으로 전화번호를 제출해 주세요. + + + +## `invalid_primary_tenant` + +The primary tenant is invalid. + +대표 Tenant를 지정한 primaryTenantId가 UUID 형식이 아닙니다. + +Tenant 조회 응답에서 대상의 UUID를 확인해 주세요. 이름이나 slug 대신 해당 UUID를 primaryTenantId에 입력해 주세요. + + + +## `invalid_profile_field` + +The profile field has an invalid value. + +프로필 필드의 값 형식이 올바르지 않습니다. + +employeeNumber, name, phone의 입력 형식을 확인해 주세요. + + + +## `invalid_qr_browser_flow_owner` + +The QR login browser flow owner is invalid. + +QR 로그인 요청이 현재 브라우저 흐름의 소유자와 연결되지 않습니다. + +QR을 표시한 원래 브라우저에서 절차를 진행해 주세요. 창이나 세션이 바뀌었으면 로그인부터 다시 시작해 주세요. + + + +## `invalid_qr_request` + +The QR login request is invalid. + +QR 로그인 요청의 입력 또는 연결 정보가 유효하지 않습니다. + +공식 로그인 화면에서 새 QR 요청을 생성해 주세요. 다른 로그인 시도의 요청 정보를 섞지 마세요. + + + +## `invalid_queue_item` + +The queue item ID must be a UUID. + +큐 항목 ID가 UUID 형식이 아닙니다. + +큐 목록 API가 반환한 항목 ID로 다시 요청하세요. + + + +## `invalid_request` + +The request is malformed or contains invalid parameters. + +요청에 잘못된 매개변수나 누락된 입력이 있습니다. + +응답의 안전한 상세 정보와 API 입력 조건을 확인해 주세요. 지적된 입력을 수정해 주세요. + + + +## `invalid_revision` + +The profile revision has an invalid format. + +revision의 형식이 올바르지 않습니다. + +조회 응답의 revision 문자열을 그대로 전달해 주세요. + + + +## `invalid_scope` + +The requested scope is invalid or not permitted. + +요청한 scope가 유효하지 않거나 현재 클라이언트에 허용되지 않습니다. + +등록된 scope 목록과 실제 요청 값을 비교해 주세요. 추가 범위가 필요하면, 서비스 관리자에게 승인을 요청해 주세요. + + + +## `invalid_session` + +The session is missing, invalid, or expired. + +요청에 필요한 세션이 없거나 유효하지 않습니다. + +원래 서비스의 로그인 화면에서 다시 인증해 주세요. 세션 값을 직접 수정하지 마세요. + + + +## `invalid_session_reference` + +The session reference is invalid. + +요청에 포함된 세션 참조가 유효하지 않습니다. + +현재 로그인에서 제공한 세션 참조인지 확인해 주세요. 오래된 참조이면, 다시 로그인해 주세요. + + + +## `invalid_source_tenant` + +The source tenant is invalid. + +가져오기 원본으로 지정한 Tenant가 활성 비공개 Tenant 조건을 충족하지 않습니다. + +원본 Tenant의 활성 상태와 공개 범위를 확인해 주세요. 조건에 맞는 원본을 선택한 뒤 가져오기 미리보기를 생성해 주세요. + + + +## `invalid_tenant_selector` + +The tenant selector is invalid. + +Tenant를 지정한 값의 형식이나 조합이 유효하지 않습니다. + +API가 허용한 Tenant 선택 필드를 확인해 주세요. 식별자와 slug를 혼동하지 마세요. + + + +## `invalid_user_id` + +The user identifier is invalid. + +사용자 식별자가 필요한 형식이나 조건에 맞지 않습니다. + +사용자 조회 응답에서 식별자를 확인해 주세요. 이름이나 이메일 대신 API가 요구한 사용자 식별자를 사용해 주세요. + + + +## `invalid_value` + +The supplied value is invalid. + +입력 필드의 값이 허용된 조건에 맞지 않습니다. + +응답에 표시한 필드와 해당 값의 제약 조건을 확인해 주세요. 허용된 값으로 수정해 주세요. + + + +## `last_membership_required` + +The user must retain one membership. + +요청대로 소속을 제거하면 유지해야 할 마지막 소속이 없어집니다. + +유지할 소속을 확인해 주세요. 계정 종료가 목적이면, 소속 제거 대신 승인된 계정 처리 절차를 요청해 주세요. + + + +## `login_blocked_by_admin` + +Login is blocked by an administrator. + +관리자가 해당 계정의 로그인을 차단한 상태입니다. + +계정 관리자에게 차단 사유와 해제 절차를 문의해 주세요. 다른 인증 경로로 차단을 우회하지 마세요. + + + +## `login_completion_conflict` + +The login completion conflicts with the current state. + +로그인 완료 요청이 이미 저장된 진행 상태와 충돌합니다. + +현재 로그인 결과를 먼저 확인해 주세요. 응답이 새 로그인을 안내하면, 기존 완료 요청을 반복하지 말고 새 절차를 진행해 주세요. + + + +## `login_policy_denied` + +The login policy denied the request. + +현재 로그인 요청이 적용 중인 정책을 충족하지 않습니다. + +응답에 제공된 사유와 로그인 조건을 확인해 주세요. 서비스 관리자에게 이용 자격을 문의해 주세요. + + + +## `login_policy_unavailable` + +The login policy is unavailable. + +서버가 로그인 허용 여부를 판단할 정책을 읽지 못했습니다. 로그인 자격이 없다는 판정은 아닙니다. + +정책 서비스 상태를 담당자에게 확인해 주세요. 정책을 읽을 수 있을 때 공식 로그인 절차를 진행해 주세요. + + + +## `login_prepare_forbidden` + +The login preparation request is not allowed. + +현재 요청의 출처나 자격이 로그인 준비 절차의 허용 조건에 맞지 않습니다. + +공식 서비스 주소와 허용된 로그인 경로를 사용했는지 확인해 주세요. 연동 구현이면, 요청 출처와 인증 방식을 검토해 주세요. + + + +## `login_smoke_route_mismatch` + +The login route does not match the expected flow. + +로그인 점검용 계정이 지정된 직접 전화번호 인증 경로를 사용하지 않았습니다. + +점검 절차에 정한 직접 전화번호 로그인 경로를 사용해 주세요. 일반 계정의 로그인 정책을 변경하지 마세요. + + + +## `m2m_policy_invalid` + +The machine to machine policy is invalid. + +애플리케이션 유형과 서버 간 통신 정책의 구성이 맞지 않습니다. + +applicationProfile과 m2mPolicy의 허용 조합을 확인해 주세요. 서비스 관리자에게 용도에 맞는 구성 검토를 요청해 주세요. + + + +## `membership_invariant_violation` + +The membership invariant is not satisfied. + +요청한 소속 변경이 유지해야 할 소속 규칙을 위반합니다. + +현재 대표 소속과 추가 소속을 함께 확인해 주세요. 응답에 표시한 규칙을 만족하도록 변경 내용을 수정해 주세요. + + + +## `membership_unavailable` + +Current tenant membership information is unavailable. + +현재 소속 정보를 조회하거나 조직 경로를 구성할 수 없습니다. + +잠시 후 다시 요청해 주세요. 오류가 계속되면, request\_id와 함께 운영자에게 문의해 주세요. + + + +## `method_not_allowed` + +The requested method is not allowed. + +해당 경로가 요청한 HTTP 메서드를 지원하지 않습니다. + +API 문서에서 경로와 HTTP 메서드를 함께 확인해 주세요. 지원되는 메서드로 호출해 주세요. + + + +## `mfa_required` + +Additional multi-factor authentication is required. + +현재 작업에 필요한 다중 인증을 완료하지 않았습니다. + +로그인 화면에서 안내한 추가 인증 수단으로 인증해 주세요. 인증 후 원래 작업을 계속해 주세요. + + + +## `mirror_not_ready` + +The identity mirror is not ready. + +조회용 복제 데이터가 요청을 처리할 준비 상태에 도달하지 않았습니다. + +복제 처리의 진행 상태를 담당자에게 확인해 주세요. 준비되지 않은 결과를 빈 목록이나 전체 통계로 사용하지 마세요. + + + +## `name_required` + +A name is required. + +요청한 자원에 필요한 이름이 없습니다. + +대상 자원을 구분할 이름을 입력해 주세요. + + + +## `network_assurance_subject_mismatch` + +The network assurance subject does not match. + +네트워크 인증에 연결된 사용자와 로그인 요청의 사용자가 다릅니다. + +네트워크 인증 계정과 현재 로그인 계정을 확인해 주세요. 동일한 사용자로 공식 인증 절차를 다시 시작해 주세요. + + + +## `not_found` + +The requested resource does not exist. + +요청한 자원을 현재 조회 범위에서 찾지 못했습니다. + +자원 식별자와 경로를 확인해 주세요. 삭제나 이동 여부는 권한이 있는 담당자에게 확인해 주세요. + + + +## `not_supported` + +The requested operation is not supported. + +서버가 해당 요청 기능을 지원하지 않습니다. + +현재 API에서 지원하는 기능을 확인해 주세요. 필요한 기능이 없으면, 서비스 담당자에게 요청해 주세요. + + + +## `oidc_linkage_unavailable` + +OIDC linkage data is unavailable. + +외부 로그인과 사용자 계정의 연결 정보를 조회하지 못했습니다. + +연결 정보 저장소 상태를 담당자에게 확인해 주세요. 이 오류만으로 새 사용자 연결을 생성하지 마세요. + + + +## `oidc_login_request_load_failed` + +The service failed to load the OIDC login request. + +서버가 OIDC 로그인 요청을 읽지 못했습니다. + +현재 로그인 요청의 유효 상태와 인증 서비스 상태를 확인해 주세요. 응답이 안내하면, 공식 서비스에서 로그인을 다시 시작해 주세요. + + + +## `oidc_subject_conflict` + +The OIDC subject conflicts with an existing identity. + +외부 로그인 주체가 기존 사용자 연결과 충돌합니다. + +어떤 계정에 외부 로그인이 연결되었는지 관리자에게 확인해 주세요. 기존 연결을 임의로 삭제하거나 다른 계정에 덮어쓰지 마세요. + + + +## `operation_in_flight` + +An equivalent operation is already in progress. + +같은 작업이 이미 처리 중입니다. + +기존 작업의 식별자로 진행 상태를 조회해 주세요. 새 요청이나 새 Idempotency-Key로 같은 작업을 중복 제출하지 마세요. + + + +## `operation_indeterminate` + +The operation result is unknown. + +서버가 작업의 성공 또는 실패를 확정하지 못했습니다. + +기존 작업 상태와 대상 자원을 먼저 조회해 주세요. 결과가 확인되기 전에는 새 Idempotency-Key로 같은 변경을 제출하지 마세요. 담당자에게 기존 요청 식별자로 확인을 요청해 주세요. + + + +## `operation_not_approved` + +The operation lacks approval. + +요청한 작업을 애플리케이션에 승인된 작업 목록에서 사용할 수 없습니다. + +요청한 OperationID와 승인된 작업 목록을 비교해 주세요. 담당 관리자에게 해당 작업의 등록과 승인 상태를 확인해 주세요. + + + +## `operation_not_found` + +The operation was not found. + +해당 사용자의 HR 작업을 찾을 수 없습니다. + +수정 응답의 operationId와 사용자 ID를 확인해 주세요. + + + +## `operations_artifact_not_found` + +The diagnostic artifact is unavailable or expired. + +결과가 없거나 만료 또는 제거되었습니다. + +작업 종류와 인스턴스를 확인하고 필요하면 새 진단 작업을 실행해 주세요. + + + +## `operations_artifact_not_ready` + +The diagnostic artifact is not ready. + +진단 결과가 아직 준비되지 않았습니다. + +작업이 끝난 뒤 다시 다운로드해 주세요. + + + +## `operations_job_capacity` + +The operations job registry is at capacity. + +인스턴스의 작업 수 제한에 도달했습니다. + +진행 중인 작업 상태를 확인하고 작업이 끝난 뒤 다시 시도해 주세요. + + + +## `operations_job_not_cancellable` + +The operations job can no longer be canceled. + +작업이 끝났거나 이미 취소 요청을 처리 중입니다. + +작업 목록을 새로 고쳐 최종 상태를 확인해 주세요. + + + +## `operations_job_not_found` + +The operations job does not exist on this instance. + +이 인스턴스에 해당 작업이 없습니다. + +작업을 시작한 인스턴스인지, 재시작이나 보관 만료가 있었는지 확인해 주세요. + + + +## `operations_queue_item_not_cancellable` + +The queue item is missing, processing, or already complete. + +큐 항목이 없거나 이미 처리 중 또는 완료 상태입니다. + +큐 목록을 새로 고친 뒤 대기 또는 실패 항목을 선택하세요. 중단은 이미 실행한 외부 작업을 되돌리지 않습니다. + + + +## `operations_queue_item_not_retryable` + +The queue item is missing, stale, or not eligible for retry. + +항목이 없거나 오래된 권한 변경이어서 재시도할 수 없습니다. + +큐 상태와 최신 권한 작업을 다시 확인해 주세요. 과거 변경을 강제로 복원하지 마세요. + + + +## `operations_queue_unavailable` + +Operations queue state is unavailable. + +큐 상태를 조회하거나 갱신하지 못했습니다. + +DB 상태와 서버 로그를 확인하고 다시 조회해 주세요. + + + +## `org_context_source_unavailable` + +The organization context source is unavailable. + +조직 정보 응답에 필요한 원본 데이터를 사용할 수 없습니다. + +담당자에게 조직 원장과 연결 서비스 상태를 확인해 주세요. 이 오류를 조직이 없다는 응답으로 처리하지 마세요. + + + +## `password_or_email_mismatch` + +The supplied password and email do not match. + +제출한 이메일과 비밀번호로 인증하지 못했습니다. 어느 값이 잘못되었는지는 이 응답으로 구분하지 않습니다. + +이메일 입력과 키보드 상태를 확인해 주세요. 비밀번호를 모르면 공식 비밀번호 복구 절차를 이용해 주세요. + + + +## `performance_history_unavailable` + +Performance history storage is unavailable. + +성능 이력 저장소를 사용할 수 없습니다. + +DB 상태를 확인한 뒤 같은 입력으로 다시 시도해 주세요. + + + +## `performance_import_too_large` + +The performance import exceeds the permitted body size. + +성능 비교 입력이 1 MiB를 초과했습니다. + +항목이나 표본 수를 줄여 1 MiB 이하의 JSON을 전달해 주세요. + + + +## `phone_number_e164_required` + +The phone number must use E.164 format. + +전화번호가 국가 코드를 포함하는 E.164 형식이 아닙니다. + +더하기 기호와 국가 코드를 포함한 전화번호를 입력해 주세요. 해당 번호의 국가별 표기 규칙을 확인해 주세요. + + + +## `pkce_restart_required` + +The authorization flow must be restarted. + +현재 인가 흐름의 코드 검증 연결을 계속 사용할 수 없습니다. + +진행 중인 인가 요청을 재전송하지 마세요. 새 코드 검증값을 사용하는 공식 로그인 절차를 처음부터 시작해 주세요. + + + +## `prerequisite_not_ready` + +A required prerequisite is not ready. + +요청 전에 완료해야 하는 준비 단계가 아직 끝나지 않았습니다. + +응답에 표시한 선행 단계의 상태를 확인해 주세요. 해당 단계가 완료된 뒤 다음 작업을 진행해 주세요. + + + +## `primary_tenant_change_not_supported` + +Changing the primary tenant is not supported. + +현재 소속 변경 API는 대표 Tenant 변경을 지원하지 않습니다. + +이 요청에서 대표 Tenant 변경을 제외해 주세요. 대표 소속 변경이 필요하면, 해당 작업을 지원하는 관리 절차를 이용해 주세요. + + + +## `primary_tenant_required` + +A primary tenant is required. + +요청한 작업에 필요한 대표 Tenant가 지정되지 않았습니다. + +현재 사용자의 대표 소속을 확인해 주세요. 권한 범위 안에서 유효한 대표 Tenant를 지정해 주세요. + + + +## `profile_fields_required` + +At least one supported profile field is required. + +수정할 프로필 필드가 없습니다. + +employeeNumber, name, phone 중 하나 이상을 전달해 주세요. + + + +## `protected_identity` + +This operation cannot access a protected identity. + +일반 사용자가 아닌 보호 계정입니다. + +일반 사용자 계정만 HR API로 조회하거나 수정해 주세요. + + + +## `protected_tenant_scope` + +The requested tenant scope is protected. + +대상 Tenant가 Directory 연동에 허용되지 않는 보호 범위에 포함됩니다. + +대상 Tenant가 보호 범위에 속하는지 확인해 주세요. 보호 범위의 변경은 승인된 관리 절차로 요청해 주세요. + + + +## `public_tenant_required` + +A public tenant is required. + +가져오기 작업에 필요한 공개 Tenant 로그인 범위가 저장되지 않았습니다. + +애플리케이션의 공개 Tenant 로그인 범위를 먼저 검토해 주세요. 담당 관리자가 해당 범위를 저장한 뒤 가져오기 미리보기를 다시 생성해 주세요. + + + +## `qr_login_not_allowed` + +QR login is not allowed for this flow. + +현재 로그인 흐름에서 QR 인증을 허용하지 않습니다. + +화면에서 허용한 인증 수단을 사용해 주세요. QR 인증이 필요하면, 서비스 관리자에게 적용 정책을 문의해 주세요. + + + +## `query_busy` + +The query capacity is busy. + +서버의 조회 처리 용량이 현재 요청을 받을 수 없는 상태입니다. + +조회 요청 수를 줄여 주세요. 서버가 안내한 대기 간격 후 읽기 요청을 다시 진행해 주세요. + + + +## `query_invalid` + +The query is invalid. + +조회 조건의 형식이나 값이 유효하지 않습니다. + +검색 필터와 정렬 및 날짜 값을 API 계약과 비교해 주세요. 잘못된 조건을 수정해 주세요. + + + +## `query_too_broad` + +The query is too broad. + +조회 대상 범위가 서버의 처리 제한을 초과했습니다. + +기간이나 Tenant 등 지원되는 조건으로 범위를 줄여 주세요. 나눈 결과를 합칠 때 중복과 누락을 확인해 주세요. + + + +## `rate_limited` + +Too many requests were received. + +요청 빈도가 서버의 허용량을 초과했습니다. + +Retry-After가 있으면, 해당 시간 동안 기다려 주세요. 반복 요청 간격을 늘려 주세요. 변경 요청은 기존 처리 상태도 먼저 확인해 주세요. + + + +## `reason_required` + +A reason is required. + +작업에 필요한 사유가 입력되지 않았습니다. + +변경 목적을 설명하는 사유를 입력해 주세요. 사유에 비밀값이나 인증 코드를 넣지 마세요. + + + +## `reauthentication_required` + +The user must authenticate again before continuing. + +현재 인증으로 작업을 계속할 수 없습니다. 이 코드만으로 만료와 명시적 폐기를 구분할 수 없습니다. + +서비스가 제공한 재인증 절차를 완료해 주세요. 설치형 갱신 API가 이 코드로 거절하면 해당 자격증명의 사용을 중단하고 PKCE 로그인을 시작해 주세요. + + + +## `refresh_in_flight` + +A session refresh is already in progress. + +같은 인증의 세션 갱신을 다른 요청이 처리하고 있습니다. + +공유 저장소를 다시 읽어 최신 자격증명을 확인해 주세요. 아직 완료되지 않았다면 Retry-After를 지키고 같은 논리 요청을 재시도해 주세요. + + + +## `replaced_by_fixed_assignment` + +The assignment was replaced by a fixed assignment. + +기존 할당 요청이 고정 할당에 의해 대체되었습니다. + +현재 적용된 고정 할당 내용을 확인해 주세요. 대체된 요청을 다시 제출하기 전에 담당자와 적용할 값을 결정해 주세요. + + + +## `replay_response_expired` + +The stored replay response expired. + +기존 작업을 재전송할 때 반환할 저장 응답의 보관 기간이 지났습니다. 원래 작업이 실패했다는 뜻은 아닙니다. + +기존 작업과 대상 자원의 상태를 먼저 조회해 주세요. 결과 확인 없이 새 Idempotency-Key로 같은 작업을 제출하지 마세요. + + + +## `request_entity_too_large` + +The request body exceeds the permitted size. + +요청 본문이 서버의 허용 크기를 초과했습니다. + +본문의 항목 수와 첨부 크기를 줄여 주세요. 일괄 작업이면, API가 허용하는 크기로 나누어 제출해 주세요. + + + +## `required` + +The required value is missing. + +필수 입력 값이 없습니다. + +응답의 필드 경로를 확인해 주세요. 해당 필드에 API가 요구하는 값을 입력해 주세요. + + + +## `required_consent_missing` + +Required consent is missing. + +현재 작업에 필요한 동의 항목이 빠져 있습니다. + +동의 화면에서 필수 항목과 요청 범위를 검토해 주세요. 동의할 항목을 선택한 뒤 절차를 완료해 주세요. + + + +## `revision_conflict` + +The user changed after the supplied revision. + +조회 이후 사용자 정보가 변경되었습니다. + +프로필을 다시 조회한 뒤 최신 revision으로 변경 내용을 확인해 요청해 주세요. + + + +## `rp_allowed_tenants_required` + +Select at least one allowed tenant when tenant access restriction is enabled. + +Tenant 접근 제한이 켜졌지만 허용할 Tenant가 없습니다. + +허용할 Tenant를 한 개 이상 선택하거나 Tenant 접근 제한을 해제해 주세요. + + + +## `rp_auto_login_url_invalid` + +Check the automatic login URL; production URLs must use HTTPS except for localhost. + +자동 로그인 URL이 Redirect URI 등록 규칙을 만족하지 않습니다. + +metadata.auto\_login\_url을 확인해 주세요. 프로덕션의 localhost 외 주소는 HTTPS를 사용해야 합니다. + + + +## `rp_backchannel_logout_uri_invalid` + +Use an absolute HTTPS Back-Channel Logout URL without credentials, fragments, or a private or local address. + +Back-Channel Logout URI가 서버에서 발송 가능한 HTTPS 주소 규칙을 만족하지 않습니다. + +HTTPS 절대 주소를 사용하고 사용자 정보·fragment·localhost·사설 IP 주소를 제외해 주세요. + + + +## `rp_headless_jwks_uri_required` + +Enter the public JWKS URL for Server-side + Headless; inline keys are not supported. + +Headless 로그인에 필요한 공개 JWKS URI가 없습니다. + +JWKS URI를 입력해 주세요. 인라인 키 대신 RP가 제공하는 JWKS URL을 사용해 주세요. + + + +## `rp_installed_policy_invalid` + +Check the installed app mode and device limits; active devices must not exceed registered devices. + +Installed 앱의 mode 또는 등록·활성 기기 수 한도가 올바르지 않습니다. + +installedAppRefreshPolicy의 mode와 기기 한도를 확인해 주세요. 활성 기기 한도는 등록 기기 한도를 초과할 수 없습니다. + + + +## `rp_installed_security_incompatible` + +Installed apps require public PKCE, device binding, authorization\_code and refresh\_token grants, and offline\_access; Headless and JWKS are not allowed. + +Installed 보안 정책과 앱 유형·인증 방식·grant·scope 설정이 충돌합니다. + +Server-side + Headless를 사용할 때 Installed 모드를 off로 설정해 주세요. Installed 앱은 public PKCE와 기기 바인딩, authorization\_code·refresh\_token grant, offline\_access scope가 필요합니다. + + + +## `rp_policy_not_active` + +The relying party policy is not active. + +요청한 서비스의 이용 정책이 활성 상태가 아닙니다. + +서비스 관리자에게 해당 애플리케이션의 정책 상태를 확인해 주세요. 승인된 정책이 활성화된 뒤 이용해 주세요. + + + +## `rp_redirect_uri_https_required` + +Use an HTTPS redirect URL in production; HTTP is allowed only for localhost. + +프로덕션에서 localhost 외의 HTTP Redirect URI가 거절되었습니다. + +reason의 필드에 표시된 Redirect URI 또는 로그아웃 후 Redirect URI를 HTTPS로 변경해 주세요. + + + +## `rp_redirect_uri_invalid` + +Enter an absolute redirect URL without userinfo or a fragment and with a valid port. + +Redirect URI의 절대 주소, 포트 또는 userinfo/fragment 조건을 만족하지 않습니다. + +reason의 배열 인덱스에 해당하는 URI를 확인해 주세요. 사용자명·비밀번호·fragment를 제외하고 유효한 절대 URL을 입력해 주세요. Installed 앱의 custom scheme은 해당 프로필 규칙을 확인해 주세요. + + + +## `rp_redirect_uri_multiple_values` + +Send each redirect URL as a separate array item; do not include line breaks within a URL. + +Redirect URI 배열의 한 항목에 줄바꿈이 포함되어 있습니다. + +각 Redirect URI를 별도 배열 항목으로 전달해 주세요. DevFront 입력에서는 쉼표 또는 줄바꿈으로 URL을 구분해 주세요. + + + +## `rp_redirect_uris_required` + +Add at least one redirect URL for this interactive application. + +Interactive 앱 생성 또는 수정 요청에 Redirect URI가 없습니다. + +인증 응답을 받을 Redirect URI를 한 개 이상 등록해 주세요. + + + +## `rp_whitelist_denied` + +The relying party is not on the allowlist. + +요청한 서비스가 허용 목록에 포함되지 않았습니다. + +서비스 식별자와 등록 상태를 확인해 주세요. 사용 승인이 필요하면, 담당자에게 허용 목록 등록 검토를 요청해 주세요. + + + +## `scan_incomplete` + +The requested scan is incomplete. + +검사 대상이 안전한 조회 한도 등을 넘어 전체 검사를 끝내지 못했습니다. + +부분 결과를 전체 정상 판정으로 사용하지 마세요. 지원되는 범위 제한이나 별도 검사 방법을 담당자에게 확인해 주세요. + + + +## `scope_tenant_not_found` + +The scope tenant does not exist. + +자격 증명 범위로 지정한 Tenant를 찾지 못했습니다. + +범위의 Tenant 식별자와 삭제 여부를 확인해 주세요. 실제 존재하는 허용 대상 Tenant를 지정해 주세요. + + + +## `scope_tenant_required` + +A tenant is required for this scope. + +선택한 범위 종류에 필요한 Tenant 식별자가 없습니다. + +Tenant 범위를 사용할 때 scope.tenantId를 지정해 주세요. 그 Tenant에 대한 권한을 확인해 주세요. + + + +## `secondary_phone_must_differ` + +The secondary phone number must differ from the primary phone number. + +보조 전화번호가 기본 전화번호와 같습니다. + +기본 번호와 다른 보조 번호를 입력해 주세요. 보조 번호가 필요 없으면, API가 허용하는 빈 값 처리 방식을 사용해 주세요. + + + +## `service_unavailable` + +The service is temporarily unavailable. + +현재 서버가 요청한 기능을 제공할 수 없습니다. + +서비스 상태와 Retry-After 안내를 확인해 주세요. 변경 요청이면, 기존 작업 결과를 조회한 뒤 재시도 여부를 결정해 주세요. + + + +## `session_expired` + +The session expired. + +서버가 세션의 유효 시간 만료를 확인했습니다. 설치형 앱에서는 짧은 작업 세션만 만료되어 복구할 수 있는 경우가 있습니다. + +next\_action이 refresh\_session이고 유효한 복구 자격증명을 보유했다면 세션 갱신 API를 호출해 주세요. 복구 자격증명이 없거나 갱신이 최종 거절되면 다시 로그인해 주세요. + + + +## `session_refresh_absolute_expired` + +The session refresh credential reached its maximum lifetime. + +설치형 복구 자격증명의 절대 기한이 지났습니다. HTTP 401의 최상위 code로 반환하며 갱신할 수 없습니다. + +next\_action=restart\_login에 따라 PKCE 로그인을 새로 시작해 주세요. 같은 복구 자격증명을 다시 보내거나 기기 시각을 바꾸어 재시도하지 마세요. + + + +## `session_refresh_idle_expired` + +The session refresh credential expired after inactivity. + +설치형 복구 자격증명의 미사용 기한이 지났습니다. HTTP 401의 최상위 code로 반환하며 갱신할 수 없습니다. + +next\_action=restart\_login에 따라 PKCE 로그인을 새로 시작해 주세요. 새 로그인으로 받은 복구 자격증명과 기한을 함께 저장해 주세요. + + + +## `session_subject_conflict` + +The session subject conflicts with the request. + +현재 세션의 사용자와 요청에 연결된 사용자가 다릅니다. + +사용할 계정과 요청의 사용자 식별자를 확인해 주세요. 올바른 계정으로 인증한 뒤 해당 계정의 요청을 생성해 주세요. + + + +## `single_operation_required` + +Exactly one operation is required. + +한 요청에 정확히 하나의 작업이 필요하지만 다른 수의 작업이 지정되었습니다. + +요청을 작업별로 분리해 주세요. 각 요청에 허용된 작업 하나만 지정해 주세요. + + + +## `single_top_level_tenant_required` + +A single top level tenant is required. + +사용자의 소속이 하나의 최상위 Tenant 아래에 있어야 하는 규칙을 충족하지 않습니다. + +대표 소속과 추가 소속의 최상위 Tenant를 비교해 주세요. 담당 관리자와 허용된 소속 구성을 결정해 주세요. + + + +## `single_value_required` + +Exactly one value is required. + +정확히 하나의 값이 필요한 필드에 다른 수의 값이 제공되었습니다. + +오류가 표시된 필드에 값 하나만 지정해 주세요. 여러 작업이 필요하면, API 계약에 맞게 요청을 분리해 주세요. + + + +## `slow_down` + +The client must slow down its polling or request rate. + +상태 조회나 반복 요청의 간격이 서버 요구보다 짧습니다. + +서버가 안내한 interval 또는 Retry-After에 따라 요청 간격을 늘려 주세요. 같은 요청을 동시에 여러 번 보내지 마세요. + + + +## `sms_send_failed` + +The service failed to send the SMS. + +서버가 인증 SMS 발송을 완료하지 못했습니다. + +메시지 수신 여부와 현재 인증 요청 상태를 먼저 확인해 주세요. 미수신 상태가 계속되면, 요청 식별자로 담당자에게 문의해 주세요. + + + +## `sms_unavailable` + +The SMS service is unavailable. + +SMS 발송 서비스가 구성되지 않았거나 현재 사용할 수 없습니다. + +담당자에게 SMS 서비스 구성과 공급자 상태 확인을 요청해 주세요. 로그인 화면에 다른 허용된 인증 수단이 있으면, 해당 절차를 이용해 주세요. + + + +## `snapshot_expired` + +The snapshot expired. + +목록 조회의 기준이 되는 저장 시점 정보가 만료되었습니다. + +기존 페이지 이동 정보를 제거해 주세요. 첫 페이지부터 새 조회를 시작해 주세요. + + + +## `source_incomplete` + +The source does not contain a complete result. + +원본에서 완전한 결과를 확보하지 못했습니다. 제공된 항목만으로 전체 상태를 판단할 수 없습니다. + +누락 범위와 조회 제한을 확인해 주세요. 완전한 결과를 확보하기 전에는 전체 합계나 일괄 변경의 근거로 사용하지 마세요. + + + +## `source_unavailable` + +The source is unavailable. + +요청에 필요한 원본 데이터를 조회할 수 없습니다. 원본이 없다는 판정은 아닙니다. + +원본 서비스의 상태를 담당자에게 확인해 주세요. 조회 실패를 빈 결과로 처리하거나 원본 삭제의 근거로 사용하지 마세요. + + + +## `stack_snapshot_unavailable` + +The goroutine stack snapshot is unavailable. + +goroutine 스택을 수집하지 못했습니다. + +진단 허용 설정과 인스턴스 상태를 확인한 뒤 다시 시도해 주세요. + + + +## `superseded_by_direct_assignment` + +The assignment was superseded by a direct assignment. + +기존 할당 요청이 직접 지정한 값으로 대체되었습니다. + +현재 직접 할당된 값을 확인해 주세요. 대체된 이전 요청을 자동으로 다시 적용하지 마세요. + + + +## `temporarily_unavailable` + +The requested service is temporarily unavailable. + +요청한 기능을 일시적으로 사용할 수 없습니다. + +서비스 상태와 대기 안내를 확인해 주세요. 변경 요청이면, 이미 처리된 결과가 있는지 먼저 확인해 주세요. + + + +## `tenant_action_scope_invalid` + +The tenant action scope is invalid. + +Tenant 작업의 적용 범위가 허용된 구성에 맞지 않습니다. + +changes.scope와 해당 작업의 허용 범위를 비교해 주세요. 권한이 있는 대상 범위로 요청을 수정해 주세요. + + + +## `tenant_affiliation_projection_mismatch` + +The tenant affiliation projection does not match the authoritative state. + +소속 원장과 조회용 소속 데이터의 내용이 일치하지 않습니다. + +관리자에게 소속 원장과 정합성 검사 결과의 비교를 요청해 주세요. 조회용 데이터에 맞추려고 원본 소속을 임의로 변경하지 마세요. + + + +## `tenant_affiliation_projection_unavailable` + +The tenant affiliation projection is unavailable. + +소속 조회용 데이터를 사용할 수 없어 원장과의 일치를 확인하지 못했습니다. + +담당자에게 소속 데이터 동기화와 저장소 상태를 확인해 주세요. 사용자에게 소속이 없다고 처리하지 마세요. + + + +## `tenant_business_registration_number_conflict` + +The business registration number conflicts with an existing tenant. + +사업자등록번호가 기존 Tenant에 등록된 번호와 충돌합니다. + +같은 사업자등록번호의 기존 Tenant를 검토해 주세요. 중복 생성 대신 승인된 중복 검토 절차를 진행해 주세요. + + + +## `tenant_domain_conflict` + +The tenant domain conflicts with an existing domain. + +지정한 도메인이 기존 Tenant의 도메인 등록과 충돌합니다. + +현재 도메인 등록 대상을 조회해 주세요. 같은 조직의 기존 Tenant인지 확인한 뒤 관리자가 등록 대상을 결정해 주세요. + + + +## `tenant_graph_unavailable` + +The tenant graph is unavailable. + +Tenant의 부모 관계와 범위를 판단할 계층 정보를 사용할 수 없습니다. + +담당자에게 Tenant 계층 원장과 조회 상태를 확인해 주세요. 계층이 확인되기 전에는 범위를 넓혀 요청하지 마세요. + + + +## `tenant_not_allowed` + +The tenant is not allowed for this operation. + +현재 요청에서 해당 Tenant를 사용할 수 없습니다. + +대상 Tenant와 현재 사용자 또는 자격 증명의 허용 범위를 비교해 주세요. 필요한 접근은 관리자에게 승인을 요청해 주세요. + + + +## `tenant_not_found` + +The requested tenant does not exist. + +요청한 Tenant를 현재 조회에서 찾지 못했습니다. + +Tenant 식별자와 조회 범위를 확인해 주세요. 삭제나 이동 여부는 관리 화면에서 확인해 주세요. + + + +## `tenant_policy_managed_centrally` + +The tenant policy is managed by a central authority. + +애플리케이션에서 변경하려는 Tenant 접근 제한은 중앙 로그인 정책에서 관리합니다. + +로그인 보안 화면에서 현재 Tenant 접근 제한을 확인해 주세요. 정책 변경 권한이 있는 담당자에게 변경을 요청해 주세요. + + + +## `tenant_public_organization_code_conflict` + +The public organization code conflicts with an existing tenant. + +공공기관 코드가 기존 Tenant의 등록과 충돌합니다. + +해당 코드가 등록된 Tenant를 확인해 주세요. 기관 식별자를 임의로 바꾸지 말고 관리자에게 중복 검토를 요청해 주세요. + + + +## `tenant_scope_unavailable` + +The tenant scope is unavailable. + +요청에 적용할 Tenant 범위를 확인할 수 없습니다. + +사용 중인 자격 증명과 대상 Tenant를 확인해 주세요. 범위 조회 장애이면, 담당자에게 복구 확인을 요청해 주세요. + + + +## `tenant_selector_required` + +A tenant selector is required. + +요청에서 대상 Tenant를 선택할 값이 빠져 있습니다. + +API가 요구하는 Tenant 식별자 또는 선택 필드를 입력해 주세요. 현재 권한 범위의 대상을 선택해 주세요. + + + +## `timezone_invalid` + +The time zone value is invalid. + +시간대 값이 서버에서 지원하는 형식이나 값이 아닙니다. + +해당 API가 허용하는 시간대 이름을 확인해 주세요. 표시용 시간대와 전송할 날짜·시간 형식을 구분해 주세요. + + + +## `totp_enrollment_required` + +TOTP enrollment is required before continuing. + +다음 작업에 필요한 TOTP 인증 수단이 등록되지 않았습니다. + +공식 보안 설정 화면에서 시간 기반 일회용 코드인 TOTP를 등록해 주세요. 등록 절차의 확인을 마친 뒤 원래 작업으로 돌아가 주세요. + + + +## `trial_claim_history_conflict` + +The stored trial claim history conflicts with the requested operation. + +저장된 RP trial 만료 이력이 요청된 값과 충돌합니다. + +기존 trial 원장과 고정 만료값을 확인해 주세요. 충돌을 해결하기 전에는 새 trial을 지급하거나 값을 덮어쓰지 마세요. + + + +## `trial_claim_managed` + +The trial claim is managed by the trial entitlement ledger. + +RP trial 만료 claim은 trial 원장에서 관리되므로 일반 metadata API로 변경할 수 없습니다. + +trial 원장의 정책과 지급 상태를 확인해 주세요. 일반 metadata 수정 요청에서 해당 claim을 제외해 주세요. + + + +## `trial_entitlement_unavailable` + +The trial entitlement ledger is unavailable. + +RP trial 지급 원장을 사용할 수 없습니다. + +서비스 관리자에게 trial 원장과 데이터베이스 상태를 확인해 주세요. 원장 장애를 새 trial 지급이나 기본 만료값으로 대체하지 마세요. + + + +## `unauthorized` + +Authentication is required for this request. + +요청에 유효한 인증 정보가 없습니다. + +해당 API가 요구하는 인증 방식을 확인해 주세요. 사용자 작업이면, 공식 로그인 화면에서 인증해 주세요. + + + +## `unexpected_browser_credentials` + +Browser credentials are not allowed for this flow. + +현재 인증 흐름은 브라우저 세션 등 대화형 인증 정보의 동시 전달을 허용하지 않습니다. + +해당 API가 요구하는 인증 정보만 전송해 주세요. 연동 구현에서 자동으로 붙이는 브라우저 쿠키나 다른 인증 헤더가 있는지 확인해 주세요. + + + +## `unprocessable_entity` + +The well formed request fails processing. + +요청 형식은 해석했지만 입력 내용이 작업 처리 조건을 충족하지 않습니다. + +응답의 필드 오류와 업무 조건을 확인해 주세요. 해당 조건에 맞게 입력 내용을 수정해 주세요. + + + +## `unsupported` + +The supplied value is not supported. + +입력 필드에 서버가 지원하지 않는 값이 지정되었습니다. + +응답의 필드 경로와 허용 값 목록을 확인해 주세요. 지원되는 값을 지정해 주세요. + + + +## `unsupported_media_type` + +The request media type is not supported. + +요청의 Content-Type을 해당 API가 지원하지 않습니다. + +API가 요구하는 Content-Type과 본문 형식을 함께 확인해 주세요. 지원되는 형식으로 전송해 주세요. + + + +## `unsupported_operation` + +The requested operation is not supported. + +현재 API나 자격 증명 체계가 요청한 작업을 지원하지 않습니다. + +지원하는 OperationID와 경로 목록을 확인해 주세요. 다른 작업을 같은 식별자로 대신 호출하지 마세요. + + + +## `upstream_outcome_unknown` + +The upstream operation result is unknown. + +연결된 인증 서비스가 로그인 완료 또는 동의 승인·거절을 처리했는지 서버가 확정하지 못했습니다. + +현재 로그인 상태를 먼저 확인해 주세요. 같은 완료 요청은 같은 키나 새 Idempotency-Key로도 재전송하지 마세요. 로그인이 필요하면, 공식 서비스에서 새 로그인 절차를 시작해 주세요. + + + +## `upstream_rate_limited` + +An upstream service rate limited the request. + +연결된 서비스가 요청 빈도를 제한했습니다. + +Retry-After가 있으면, 안내한 시간 동안 기다려 주세요. 변경 요청이면, 기존 처리 결과를 확인한 뒤 API가 허용하는 재시도 방식을 사용해 주세요. + + + +## `upstream_response_invalid` + +The upstream service returned an invalid response. + +연결된 서비스의 응답을 예상한 형식이나 조건으로 처리할 수 없습니다. + +담당자에게 요청 식별자와 응답 오류를 전달해 주세요. 변경 요청이면, 연결된 서비스의 처리 결과를 먼저 확인해 주세요. + + + +## `upstream_unavailable` + +The upstream service is unavailable. + +연결된 서비스가 요청 처리에 필요한 기능을 제공하지 못했습니다. + +담당자에게 연결 서비스의 상태를 확인해 주세요. 변경 요청이면, 실제 처리 결과를 조회한 뒤 후속 요청을 결정해 주세요. + + + +## `upstream_unreachable` + +The upstream service is unreachable. + +서버가 연결된 서비스에 도달하지 못했습니다. + +담당자에게 서비스 주소와 네트워크 연결 상태를 확인해 주세요. 변경 요청이면, 기존 작업의 처리 상태부터 확인해 주세요. + + + +## `user_already_exists` + +The user already exists. + +등록하려는 사용자가 기존 사용자와 중복됩니다. + +기존 사용자 목록에서 같은 식별자를 확인해 주세요. 새 계정 생성 대신 기존 계정의 이용 또는 복구 절차를 검토해 주세요. + + + +## `user_not_found` + +The requested user does not exist. + +요청한 사용자를 현재 조회 범위에서 찾지 못했습니다. + +사용자 식별자와 조회 범위를 확인해 주세요. 사용자 삭제 여부는 권한이 있는 관리자에게 확인해 주세요. + + + +## `validation_failed` + +One or more request fields failed validation. + +하나 이상의 요청 필드가 입력 검사를 통과하지 못했습니다. + +응답의 필드별 오류 목록을 확인해 주세요. 지적된 필드의 값과 형식을 수정해 주세요. + + + +## `worksmobile_visibility_conflict` + +The Worksmobile visibility conflicts with the requested operation. + +Worksmobile에서 비공개인 상위 조직 아래에 공개 조직을 지정하려고 했습니다. + +config.worksmobileVisible 값과 상위 조직의 노출 규칙을 확인해 주세요. 담당 관리자에게 허용된 노출 구성의 검토를 요청해 주세요. + diff --git a/docs/sso_guides/contributor.md b/docs/sso_guides/contributor.md new file mode 100644 index 0000000..70afff0 --- /dev/null +++ b/docs/sso_guides/contributor.md @@ -0,0 +1,255 @@ +# Contributor Guide + +## 준비 + +- `.env.sample`에서 로컬 `.env`를 만든다. +- secret을 commit하거나 issue log에 복사하지 않는다. +- Docker network와 Ory 설정은 Make target으로 생성·검증한다. + +```bash +make build-auth-config +make validate-auth-config +make verify-auth-config +``` + +## 실행 + +전체 개발 stack: + +```bash +make dev +``` + +Frontend 개발 의존 stack: + +```bash +make up-front-dev +``` + +UserFront WASM 개발: + +```bash +make dev-wasm +``` + +`ory-net`은 Ory 내부 통신, `baron_net`은 application 통신, public network는 공개 gateway 경계다. Ory Admin port를 임의로 host에 공개하지 않는다. + +## 테스트 + +원격 검사의 역할과 결과 해석은 [README의 Code Check CI](../../README.md#-code-check-ci)를 확인해 주세요. 소속 회귀와 야간 E2E, 성능 측정은 각각 독립된 검사를 수행합니다. Actions에서 작업을 생략했다면 해당 실행의 통과 건수에 포함하지 마세요. + +전체 로컬 CI 상당 검사: + +```bash +make code-check +``` + +영역별 target: + +```bash +make code-check-backend-tests +make code-check-userfront-tests +make code-check-adminfront-tests +make code-check-devfront-tests +make code-check-orgfront-tests +make code-check-userfront-e2e-tests +``` + +버그 수정은 다음 순서를 사용한다. + +1. 실제 사용자 경로를 재현하는 실패 테스트를 추가하거나 확인한다. +2. RED를 기록한다. +3. 최소 변경으로 수정한다. +4. 관련 test와 상위 regression suite를 통과시킨다. +5. 원인, 변경, 증거를 이슈에 기록한다. + +별도 `test-plan` 또는 `trouble-shooting` Markdown을 만들지 않는다. + +### 실제 Ory 인증 API E2E + +로그인 회귀 검사의 기본 진입점은 다음과 같습니다. 저장소 루트에서 실행하며, 아래의 개별 MFA 검사도 함께 호출합니다. + +```bash +(cd userfront-e2e && npm ci) +bash test/auth_login_matrix_harness_test.sh +AUTH_LOGIN_MATRIX_OUTPUT_DIR=reports/auth-login-matrix \ + bash test/harness/auth_login_matrix.sh +``` + +Go·로컬 Docker Engine·Bash·curl 외에 Node.js 24가 필요합니다. 브라우저 검사는 Docker Playwright를 사용합니다. 외부 WORKS/SMS/메일 전송은 대역으로 처리하며 운영 계정에 메시지를 보내지 않습니다. 각 실행의 테스트 계정·Ory 저장소는 로컬에서 격리합니다. + +| 검사 경계 | 실행 대상 | +| --- | --- | +| 공개 비밀번호·WORKS·SMS·이메일·QR 로그인 | 실제 Kratos 세션, 중앙 정책, Hydra consent, 제품 TokenFacade 교환 및 세션 누락 거부 | +| 공개 PKCE·비공개 Basic/Post RP | 정상 교환, verifier 누락·불일치, redirect·client 불일치, code 재사용, 잘못된 secret 거부 | +| RP 갱신·검증·폐기 | 실제 refresh 발급·갱신과 IntrospectionFacade, 폐기 후 token 미발급 | +| Headless 비밀번호·전화번호·링크 | 실제 Ory, RP별 인증과 consent, SDK·사용자 정의 동의 UI | +| MFA | 일반 계정·Super Admin의 정책별 허용·거부 | +| M2M `private_key_jwt` | 실제 Hydra 발급, audience·signature·client·만료·재사용 증명 거부 | +| 설치형 RP | 실제 PostgreSQL·서명·암호화·장치/사용자 분리, 외부 Hydra는 HTTP 대역 | +| RP 콜백 | 실제 `oidc-client-ts`의 state·nonce·재사용 처리, 독립 로컬 issuer 대역 | +| Back-Channel Logout | 제품 JWT 생성·전송, 테스트 RP의 서명 검증과 대상 세션 종료·다른 세션 보존 | +| 브라우저 세션 보존·복구 | [UserFront E2E 실행 방법](../../userfront-e2e/README.md)의 별도 Docker 명령 | + +`AUTH_LOGIN_MATRIX_OUTPUT_DIR`를 생략하면 로그는 출력에 표시되는 임시 디렉터리에 남습니다. 기본 진입점은 필수 테스트 누락·SKIP·하위 명령 실패를 성공으로 처리하지 않습니다. Code Check의 백엔드 검사에서도 같은 진입점을 실행하고 `backend-coverage-report` 산출물에 로그를 포함합니다. 백엔드 인증 구현이나 테스트 실행 환경이 바뀌면 관련 UserFront E2E도 선택됩니다. + +같은 브라우저 fixture를 사용하는 검사를 서로 다른 명령으로 동시에 실행하지 마세요. 각 테스트의 reset 요청이 다른 실행의 상태를 지울 수 있습니다. 전체 검사와 부분 검사는 순차 실행하고, 다른 환경을 쓸 때는 컨테이너·포트·출력 디렉터리를 모두 분리해 주세요. + +통과는 아래에 명시한 테스트 경계의 결과입니다. 운영 WORKS/SMS 수신, 모든 RP의 실제 설정과 기기·브라우저 조합까지 검증했다는 뜻은 아닙니다. 실행 증거와 미해결 항목은 [#2131](https://gitea.hmac.kr/baron/baron-sso/issues/2131)에 기록합니다. + +테스트 전용 RP는 `test/rp-demo`에 있으며 별도 배포 앱이 아닙니다. 실제 `UserManager`의 콜백 동작을 로컬 issuer와 검증하고, 실제 Hydra 프로토콜 교환은 Go matrix가 별도로 검증합니다. 이 두 검사를 AdminFront의 실제 운영 로그인 왕복이나 모든 ID token 검증 항목의 증거로 합쳐 설명하지 않습니다. + +인증 거부와 오류 응답 분류는 구분합니다. Hydra v26.2.0의 만료 M2M 증명 500 응답은 [#2134](https://gitea.hmac.kr/baron/baron-sso/issues/2134), 폐기된 refresh의 중앙 정책 오류 분류는 [#2135](https://gitea.hmac.kr/baron/baron-sso/issues/2135)에서 추적합니다. 해당 사례는 현재의 정확한 응답과 token 미발급을 함께 검사하며, 다른 음성 사례를 임의의 5xx로 통과시키지 않습니다. + +배포 전에 로컬에서 인증 흐름을 확인하려면 저장소 루트에서 다음 명령을 실행해 주세요. Linux의 로컬 Docker Engine 접근 권한, Go 1.26.2 이상, Bash, curl이 필요합니다. 원격 Docker context는 거부합니다. 첫 실행에는 PostgreSQL·Kratos·Hydra 이미지 다운로드와 Go 모듈 다운로드를 위한 네트워크 접근이 필요할 수 있습니다. + +```bash +bash test/harness/issue2061_real_ory.sh +``` + +실행할 때마다 별도 PostgreSQL·Kratos·Hydra 컨테이너와 테스트 계정을 생성합니다. Ory public/admin 및 DB 포트는 테스트 동안 `127.0.0.1`의 동적 포트에만 연결되며, 종료 시 해당 컨테이너와 임시 DB를 제거합니다. 기존 개발 스택이나 원격 계정은 사용하지 않습니다. + +이 명령은 실제 MFA 정책 판정, 세션 binding 저장, authorization code/PKCE 토큰 발급, Backend `/api/v1/user/sessions` 조회를 연결합니다. 일반 계정과 Super Admin 모두 TOTP 등록 여부별 미설정·비활성·권장의 AAL1 허용, 필수의 AAL2 허용, 미등록·AAL1의 필수 MFA 발급 거부를 검사합니다. 모든 RP 화면이나 개별 업무 권한 검증을 대체하지는 않습니다. + +Backend가 MFA를 판정하는 경로를 검증하도록 로컬 [Kratos fixture](../../test/harness/issue2061_kratos.yml)의 `session.whoami.required_aal`은 `aal1`로 고정합니다. 운영 Ory 설정과의 정합성은 [#2065](https://gitea.hmac.kr/baron/baron-sso/issues/2065)의 별도 확인 사항입니다. 이 명령의 통과만으로 실제 배포 환경의 설정까지 검증되었다고 판단하지 마세요. + +종료 코드가 `0`이면 통과입니다. 스크립트가 테스트 실패·누락·SKIP과 컨테이너 정리 실패를 검사하므로 결과 파일을 수동 집계할 필요는 없습니다. 출력에 표시되는 임시 디렉터리의 `result.jsonl`에는 Go test JSON 이벤트가 남으며 자동 삭제하지 않습니다. + +일반 종료·실패·중단 시 정리하지만, `SIGKILL`이나 Docker 장애 시에는 정리가 보장되지 않습니다. 시작 시 출력된 테스트 식별자로 `docker ps -a --filter label=baron.issue2061=<테스트-식별자>`를 조회하고, 해당 실행의 컨테이너 ID만 확인해 정리해 주세요. + +인증 정책 정본은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow), 실행 증거는 [#2061](https://gitea.hmac.kr/baron/baron-sso/issues/2061)에 기록합니다. 배포 workflow/controller에 자동 검사나 배포 차단 조건을 추가하지 않는 독립 진단 명령입니다. + +### RP whitelist 가져오기 실제 연동 E2E + +저장소 루트에서 실행합니다. Linux의 로컬 Unix socket Docker Engine, Go 1.26.2 이상, Node.js 24 이상, npm, Bash, curl, ripgrep이 필요합니다. 최초 준비 시 DevFront 의존성을 설치합니다. + +```bash +(cd devfront && npm ci) +bash test/harness/rp_login_import_live.sh +``` + +[실행기](../../test/harness/rp_login_import_live.sh)는 합성 계정과 전용 Kratos·Hydra·Keto·PostgreSQL 컨테이너를 생성하고 Playwright Docker로 실제 로그인 보안 화면을 검사합니다. 최초 실행에는 이미지·패키지·Go 모듈 다운로드가 필요할 수 있습니다. Backend 기본 포트 `127.0.0.1:18095`와 DevFront `127.0.0.1:5195`가 비어 있어야 합니다. 변경하려면 `RP_LOGIN_IMPORT_BACKEND_ADDR=127.0.0.1:<포트>`와 `RP_LOGIN_IMPORT_WEB_PORT=<포트>`를 해당 명령에 지정합니다. Ory·DB는 loopback 동적 포트를 사용합니다. + +[브라우저 테스트](../../devfront/tests/rp-login-member-import-live.spec.ts)는 API 응답을 모의 처리하지 않습니다. 실제 Kratos 세션을 브라우저 저장소에 주입하고 [테스트 서버](../../backend/internal/handler/rp_login_import_live_browser_test.go)에 연결된 제품 handler와 실제 서비스의 응답·DB 결과를 확인합니다. 로그인 화면을 통한 OIDC 왕복과 `main.go`의 전체 middleware 조립은 검증하지 않습니다. 기존 mock 화면 테스트는 로딩·오류 표시 등 UI 회귀 검사로 사용하며 실제 권한 판정의 증거와 구분합니다. + +검사 대상은 super_admin·권한 있는 관리자의 가져오기, 중복 추가 방지, 원본 소속 제거 후 whitelist 유지, 활성화 설정 유지, 원본·RP 권한 거부, 미리보기 이후 정책 변경·권한 회수에 따른 적용 거부입니다. 거부 응답은 HTTP 상태와 DB 미변경을 함께 확인합니다. + +종료 코드 `0`은 필수 Go 테스트와 브라우저 검사 통과 및 컨테이너 정리 확인을 뜻합니다. 이 명령은 테스트 누락·SKIP을 성공으로 처리하지 않습니다. 브라우저 spec만 단독 실행하면 live 환경이 없어 SKIP될 수 있으므로 위 실행기를 사용합니다. 출력된 임시 디렉터리의 `playwright.json`에서 결과를, `evidence/results/`에서 화면 캡처를 확인합니다. `backend.jsonl`과 기타 원시 로그에는 합성 인증정보가 포함될 수 있으므로 그대로 공유하지 않습니다. + +일반 종료 시 해당 실행의 컨테이너와 임시 DB를 제거하고 결과 파일은 유지합니다. 강제 종료나 Docker 장애로 자원이 남으면 출력된 식별자로 `docker ps -a --filter label=baron.rp-import=<테스트-식별자>`를 조회해 해당 실행의 컨테이너만 정리합니다. 실행 증거는 [#2158](https://gitea.hmac.kr/baron/baron-sso/issues/2158)에 기록합니다. + +### 사용자 projection·Kratos 성능 측정 + +저장소 루트에서 다음 명령을 실행해 주세요. Linux의 로컬 Docker Engine 접근 권한, Go 1.26.2 이상, Bash, curl, Node.js 24가 필요합니다. 실행기는 [performance_probe.sh](../../test/harness/performance_probe.sh)입니다. + +```bash +bash test/harness/performance_probe.sh +``` + +전용 Redis·PostgreSQL·Kratos 컨테이너와 합성 데이터를 생성합니다. 컨테이너마다 CPU 1개·메모리 512MiB를 제한하고 루프백 임의 포트를 사용합니다. 일반 종료 시 해당 실행의 컨테이너·볼륨·네트워크를 정리합니다. 기존 개발·운영 저장소를 대상으로 테스트 환경 변수를 지정하지 말고 제공 실행기 또는 아래의 전용 컨테이너 예제를 사용해 주세요. + +사용자 1천·1만 건에서 목록·검색·정렬의 서비스 응답시간, Redis 명령 수, Go CPU·메모리 할당·GC를 측정합니다. Kratos 목록·단건 조회는 같은 진단 client에서 계측 OFF→ON→OFF 순서로 실행해 응답 결과가 보존되는지 확인합니다. 이 ON/OFF는 진단 실행기 안의 HTTP 계측이며 실행 중인 제품 서버 설정을 변경하지 않습니다. + +출력에 표시되는 `reports/performance/<실행별 디렉터리>/`에서 결과를 확인해 주세요. + +- `go-test.log`: 원시 응답시간 표본과 집계값을 포함한 Go test JSON 이벤트 +- `docker-stats.jsonl`: 전용 컨테이너의 CPU·메모리·I/O 표본 +- `phases.jsonl`, `metadata.txt`: 실행 구간과 환경 정보 +- `exit-code.txt`: 실행 종료 코드. `0`이면 실행기의 검증을 통과한 것입니다. + +컴파일·데이터 생성 구간과 실제 측정 구간을 구분해서 읽어 주세요. 측정은 warm 상태·동시성 1의 서비스 기준선이며 실제 로그인·권한·gateway·브라우저 전체 응답시간을 뜻하지 않습니다. 메모리 할당량은 상주 메모리와 다르며, 짧은 case의 서버 CPU ON/OFF 차이를 2초 간격 Docker 표본만으로 판단하지 마세요. 상세 결과는 [#2146](https://gitea.hmac.kr/baron/baron-sso/issues/2146), 실제 화면·API 측정은 [#2148](https://gitea.hmac.kr/baron/baron-sso/issues/2148)에서 추적합니다. + +#### 테넌트 인덱스 조회 조건과 회귀 검증 + +목록 조회는 tenant scope 정규화·교집합을 요청당 한 번 계산합니다. 유효한 scope가 1~8개이고 후보 조회가 4,096명 예산 안에서 완료되면 기존 tenant SET으로 후보를 좁히고 canonical created_at ZSET 순서로 처리합니다. scope 또는 후보 조회 예산을 초과하거나, scope 제한이 없거나, 범위 밖 super_admin을 포함해야 하거나, 인덱스 후보가 비어 있으면 기존 전체 조회로 전환합니다. 빈 scope 교집합은 기존 필터에서 거부합니다. 이 예산은 반환 결과의 상한이 아닙니다. + +기존 역할·검색·scope·제외 ID·정렬·커서 처리는 유지합니다. 중앙 쓰기 경로의 인덱스 정합성을 전제로 하며, 부분적으로 누락된 tenant SET을 탐지하는 기능은 아닙니다. 조건의 코드 정본은 [후보 조회 구현](../../backend/internal/service/identity_mirror_tenant_page.go), 개선 결과와 검증 증거는 [#2149](https://gitea.hmac.kr/baron/baron-sso/issues/2149)를 참고해 주세요. + +위 성능 실행기는 전용 Redis DB 1에서 [후보 경계 통합 테스트](../../backend/internal/service/identity_mirror_tenant_page_integration_test.go)도 실행합니다. 후보 4,095·4,096·4,097명, scope 8·9개, 테넌트 이동·삭제 이후의 결과와 total을 검증합니다. 할당 예산과 목록 계약 회귀만 빠르게 실행하려면 저장소 루트에서 다음 명령을 사용해 주세요. + +```bash +go -C backend test ./internal/service \ + -run 'TestIdentityMirrorPageScopeAllocationBudget|TestRedisServiceTenantPage' -count=1 +``` + +[HTTP 통합 테스트](../../backend/internal/handler/user_handler_projection_integration_test.go)는 별도 실행입니다. 실제 Redis 저장부터 목록 HTTP handler의 JSON 응답까지 일반·서명·정렬 커서, 검색, scope 밖 사용자 제외를 검증합니다. 인증 profile·Keto·tenant 서비스는 대역이므로 전체 인증·브라우저 E2E와 구분해 주세요. + +저장소 루트에서 다음 Bash 예제를 실행하면 새 Redis 컨테이너의 빈 DB 0으로 HTTP 통합 테스트를 실행하고 종료 시 컨테이너를 정리합니다. `PERF_HANDLER_REDIS_ADDR`와 `PERF_PROBE_ALLOW_FIXTURES=1`은 이 테스트 프로세스에만 전달합니다. 재실행할 때도 새 컨테이너를 사용해 주세요. + +```bash +bash <<'BASH' +set -euo pipefail +probe_container="projection-http-$(date +%s)-$$" +trap 'docker rm -fv "$probe_container" >/dev/null' EXIT +docker run -d --name "$probe_container" --cpus=1 --memory=512m \ + -p 127.0.0.1::6379 redis:7-alpine redis-server --save '' --appendonly no >/dev/null +probe_port="$(docker port "$probe_container" 6379/tcp | sed -E 's/.*:([0-9]+)$/\1/')" +probe_ready=0 +for _ in {1..30}; do + if [[ "$(docker exec "$probe_container" redis-cli ping 2>/dev/null)" == PONG ]]; then + probe_ready=1 + break + fi + sleep 0.2 +done +[[ "$probe_ready" == 1 ]] || { echo '전용 Redis 준비 실패' >&2; exit 1; } +PERF_HANDLER_REDIS_ADDR="127.0.0.1:$probe_port" PERF_PROBE_ALLOW_FIXTURES=1 \ + go -C backend test -race ./internal/handler \ + -run '^TestUserHandlerListUsersProjectionHTTPIntegration$' -count=1 -v -timeout=3m +BASH +``` + +일반 종료·실패 시 정리하지만 `SIGKILL`이나 Docker 장애 시에는 자동 정리가 보장되지 않습니다. 남은 자원은 해당 실행의 `projection-http-` 컨테이너 이름을 확인한 뒤 정리해 주세요. + +## 오류 코드 등록 + +코드와 영문 설명의 정본은 [오류 코드 등록부](../../backend/internal/response/error_code_registry.go)입니다. +공개 응답 필드와 정보 노출 원칙은 [Error Handling Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/Error-Handling-Policy)를 따릅니다. +Swagger와 ReDoc은 [OpenAPI](../../backend/docs/openapi.yaml)에 생성된 코드 설명을 표시합니다. +코드별 원인과 조치는 [한국어 조치 자료](../../backend/cmd/error-code-registry/error_guidance.json)에서 관리합니다. +문서 생성기는 이 자료와 등록부를 결합해 [API 오류 안내](api-errors.md)와 서버용 HTML을 만듭니다. +문서 기능이 켜진 서버는 `/docs/error-codes`와 API 접두사가 붙은 같은 경로에서 HTML을 제공합니다. +기본 API 접두사를 쓰면 `/api/docs/error-codes`에서 안내를 열 수 있습니다. + +새 오류 코드를 추가할 때 다음 순서로 작업하세요. +RED는 새 요구를 만족하지 못해 테스트가 실패한 상태입니다. + +1. 실제 실패 입력으로 재현 테스트를 작성하세요. +2. 재현 테스트를 실행해 RED 상태를 확인하세요. +3. 등록부에 코드와 공개용 영문 설명을 추가하세요. +4. 한국어 조치 자료에 해당 코드의 `detail`과 `action`을 추가하세요. +5. 확인한 원인을 해당 코드로 반환하도록 구현하세요. +6. `backend` 디렉터리에서 다음 명령으로 문서와 검사를 갱신하세요. + +```sh +go run ./cmd/error-code-registry +go run ./cmd/error-code-registry -check +GOCACHE=/tmp/baron-sso-go-cache go test ./internal/response ./internal/architecture -count=1 +``` + +등록 검사는 미등록 코드, 중복 코드, 설명 누락과 문서 불일치를 검사합니다. +생성기의 `-check`는 OpenAPI, Markdown, HTML이 생성 결과와 같은지 확인합니다. +조치 자료에 등록되지 않은 코드나 비어 있는 원인 또는 조치가 있으면 생성기가 실패합니다. +영문 설명의 의미는 코드 검토에서 확인하며 ASCII 검사만으로 판단하지 않습니다. +해당 API의 재현 테스트도 다시 실행해 통과하는지 확인하세요. + +기존 `make code-check-backend-tests`는 등록 검사가 포함된 Go 테스트를 실행합니다. +생성된 설명은 직접 편집하지 않습니다. 이 검사를 배포 흐름에 추가하지 않습니다. + +## 로그와 i18n + +- production 기본 log는 필요한 `WARN/ERROR` 중심으로 유지한다. +- debug flag는 로컬 또는 제한된 진단에서만 사용한다. +- token, cookie, password, private key, credential을 항상 마스킹한다. +- Backend는 사용자 번역 문구가 아니라 error code를 반환한다. +- Frontend locale resource와 key consistency test를 함께 갱신한다. + +## 문서 + +- 정책 변경: 원격 Wiki +- 설계 불변식: `docs/principles` +- 사용자 작업: `docs/guides` +- offline 복구: `docs/runbooks` +- 이슈별 기록: Gitea 이슈 + +문서 소유 규칙은 [`docs/AGENTS.md`](../AGENTS.md)를 따른다. diff --git a/docs/sso_guides/directory-api.md b/docs/sso_guides/directory-api.md new file mode 100644 index 0000000..08071c3 --- /dev/null +++ b/docs/sso_guides/directory-api.md @@ -0,0 +1,54 @@ +# Directory API Guide + +정책 정본은 [Integration Credential 및 Directory API 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Integration-Credential-%EB%B0%8F-Directory-API-%EC%A0%95%EC%B1%85)이다. 정확한 schema는 Backend OpenAPI를 따른다. + +## Credential 준비 + +1. AdminFront의 API Key 관리에서 Directory 연동 credential을 발급한다. +2. exact OperationID와 concrete Tenant subtree scope를 선택한다. +3. 만료일과 발급 사유를 기록한다. +4. secret은 발급 시 한 번만 안전하게 전달하고 log나 issue에 남기지 않는다. +5. 교체 후 이전 secret이 무효인지 확인한다. + +`/admin/*`에 integration credential을 사용하지 않는다. interactive cookie/token과 한 요청에 혼합하지 않는다. + +## 사용자 생성 + +요청 필드, credential 발급, idempotency, 오류 처리와 검증 절차는 [외부 사용자 계정 생성 상세 가이드](integration-directory-user-create-api-guide.md)를 따른다. + +- Endpoint: `POST /api/v1/integrations/directory/users` +- OperationID: `directory.user.create` +- credential scope 안의 concrete Tenant를 지정한다. +- identity, status, 허용 profile field만 보낸다. +- System Role, raw Keto relation, protected subtree를 payload로 주입하지 않는다. +- `Idempotency-Key`를 logical create 요청에 결속한다. + +timeout 뒤 같은 payload와 idempotency key로 재시도한다. 다른 payload를 같은 key로 보내지 않는다. + +## Membership 변경 + +- Endpoint: `PATCH /api/v1/integrations/directory/users/{userId}/memberships` +- source와 destination이 credential scope에 포함되는지 확인한다. +- add와 remove 대상을 exact UUID로 전달한다. +- representative Tenant 변경이나 계정 삭제를 Membership 변경에 섞지 않는다. + +## 오류 처리 + +- `401`: secret, 만료, revoke 상태 확인 +- `403`: OperationID, exposure, Tenant scope 확인 +- `409`: idempotency fingerprint 또는 Membership invariant 확인 +- `422`: field와 business validation 확인 +- `503`: 원장/readiness 장애이므로 사용자를 없다고 처리하지 않음 + +오류 뒤 새 사용자를 임의 생성하기 전에 기존 idempotency 결과와 대상 identity를 조회한다. + +## 운영 종료 + +1. acceptance용 임시 사용자와 Membership을 명시적으로 정리한다. +2. 임시 credential을 revoke한다. +3. revoked secret으로 호출이 거부되는지 확인한다. +4. audit에서 OperationID, actor, Tenant, 결과가 기록되고 secret이 없는지 확인한다. + +조직 조회 연동은 [Org Context API Guide](org-context-api.md)를 따른다. + +한맥가족의 기존 사용자 사번·이름·전화번호 수정은 별도 [내부 인사 사용자 정보 수정 가이드](hr-profile-api.md)를 따릅니다. 일반 Directory 키의 접근 범위는 바뀌지 않습니다. diff --git a/docs/sso_guides/hr-profile-api.md b/docs/sso_guides/hr-profile-api.md new file mode 100644 index 0000000..4f4435d --- /dev/null +++ b/docs/sso_guides/hr-profile-api.md @@ -0,0 +1,85 @@ +# 내부 인사 사용자 정보 수정 + +이 가이드는 외부 인사 도구에서 기존 SSO 사용자의 사번·이름·전화번호를 수정하는 절차입니다. 사용자 연결에는 SSO의 `userId`를 사용합니다. 사번이나 이메일을 고유 사용자 키로 사용하지 않습니다. + +정확한 요청·응답은 [OpenAPI](../../backend/docs/openapi.yaml)를 따릅니다. 구현과 검증 기록은 [#2506](https://gitea.hmac.kr/baron/baron-sso/issues/2506)에 있습니다. 기존 [Directory API](directory-api.md)의 외부 사용자 생성·소속 변경과 별도 경로입니다. + +## 연결 준비 + +1. AdminFront의 `API 키 관리`에서 `내부 인사 연동`을 선택합니다. +2. 한맥가족의 대상 조직을 선택합니다. +3. `내부 인사 사용자 조회` 키와 `내부 인사 사용자 수정` 키를 각각 발급합니다. +4. 도구에 키를 별도로 보관하고 브라우저 코드·로그·CSV에 넣지 않습니다. +5. 외부 인사 사용자와 SSO `userId`의 연결을 확인합니다. + +조회 키는 `hr.user.profile.read`, 수정 키는 `hr.user.profile.update`를 사용합니다. 현재 대표 소속이 키의 조직 범위 안에 있어야 합니다. Stealth와 시스템 관리자 계정은 이 경로에서 처리하지 않습니다. 기존 일반 Directory 키를 대신 사용할 수 없습니다. + +전체 대상의 `userId`가 필요하면 기존 [조직 조회 API](org-context-api.md)의 `members[].id`를 사용합니다. 이 조회에는 별도의 `baron.org-context.read` 키와 허용된 조직 범위가 필요합니다. 외부 인사 자료와 연결할 때 여러 사용자가 같은 이름을 쓰거나 사번이 비어 있으면 자동으로 일치시킨 것으로 처리하지 않습니다. + +## 조회와 수정 + +아래 요청의 호스트는 사용 중인 SSO 주소로 지정합니다. `userId`에는 SSO 사용자 UUID를 넣습니다. 먼저 조회 키로 현재 정보를 읽습니다. + +```http +GET /api/v1/integrations/hr/users/{userId}/profile +X-Baron-Key-ID: +X-Baron-Key-Secret: +``` + +응답의 `revision`은 조회한 사용자 정보의 버전입니다. 이 값을 `expectedRevision`에 넣고 수정 키로 변경을 요청합니다. 사번 앞자리의 0을 보존하려면 JSON 문자열로 전달합니다. + +```http +PATCH /api/v1/integrations/hr/users/{userId}/profile +X-Baron-Key-ID: +X-Baron-Key-Secret: +Idempotency-Key: +Content-Type: application/json + +{ + "expectedRevision": "", + "employeeNumber": "001234" +} +``` + +필드를 생략하면 기존 값은 유지됩니다. 사번에 `null` 또는 빈 문자열을 보내면 사번을 삭제합니다. 이름은 비어 있지 않은 문자열이며, 전화번호는 `+821012345678`처럼 국가번호를 포함한 E.164 문자열입니다. 이름·전화번호의 `null`과 빈 문자열은 거절됩니다. + +이 경로는 `employeeNumber`, `name`, `phone`만 수정합니다. 부서·직급·직책·입퇴사 상태·비밀번호·권한 변경을 같은 요청에 넣지 않습니다. 기존 사번 호환 키는 서버가 함께 맞춥니다. + +## 결과 확인과 재시도 + +수정 응답의 `operationId`와 `statusUrl`을 저장합니다. `200`은 SSO 저장 결과이며 WORKS 반영 완료를 뜻하지 않습니다. `202`이면 저장 결과 확인이 필요하므로 조회 키로 `statusUrl`을 조회합니다. + +```http +GET /api/v1/integrations/hr/users/{userId}/operations/{operationId} +X-Baron-Key-ID: +X-Baron-Key-Secret: +``` + +응답은 SSO 저장 상태, 저장 후 처리 상태, WORKS 작업 상태를 구분합니다. `worksmobileStatus=verified`는 해당 작업의 원격 검증 완료를 뜻합니다. `pending`과 `processing`은 대기 상태입니다. `failed`와 `not_enqueued`는 원격 반영 완료가 아니므로 원인을 확인합니다. `superseded`는 더 최신 변경으로 교체된 작업이므로 이전 작업의 반복 조회를 끝내고 최신 작업을 확인합니다. 자세한 WORKS 작업 처리는 [WORKS 연동 Runbook](../runbooks/worksmobile.md)을 따릅니다. + +| 상태 필드 | 값과 처리 | +| --- | --- | +| `storageStatus` | `committed`는 SSO 저장 확정입니다. `unknown`은 결과 미확정이며 새 작업으로 중복 요청하지 않습니다. `not_committed`는 저장 확정 전 상태입니다. | +| `projectionStatus` | `pending`은 조회용 사본 반영 대기, `applied`는 반영 완료, `failed`는 반영 실패입니다. | +| `postCommitStatus` | `pending`은 후속 처리 대기, `applied`는 처리 완료, `failed`는 재처리 대상, `blocked`는 운영 확인이 필요한 중단 상태입니다. | + +도구는 대기 중인 작업의 ID를 보존하고 제한된 간격으로 상태를 조회합니다. 대기 시간이 도구의 제한을 넘으면 작업을 대기 목록에 남깁니다. `unknown`을 실패로 단정하거나 새 키로 같은 변경을 다시 보내지 않습니다. + +| 결과 | 도구의 처리 | +| --- | --- | +| 응답을 받지 못함 | 같은 본문과 같은 `Idempotency-Key`로 재시도합니다. | +| `409` revision 충돌 | 현재 정보를 다시 조회하고 변경 내용을 비교한 뒤 새 작업으로 요청합니다. | +| `409` 재시도 내용 충돌 | 기존 작업 키에 다른 본문을 사용했는지 확인합니다. | +| `409` 식별자 충돌 | 다른 계정이 해당 로그인 식별자를 사용하는지 확인합니다. | +| `401` 또는 `403` | 키의 상태·Operation·현재 조직 범위를 확인합니다. | +| `503` | 조회 또는 저장소 장애를 확인하고 기존 작업 결과부터 확인합니다. | + +전체 사번 갱신 도구도 사용자마다 조회·수정·결과 확인을 수행합니다. 각 사용자 변경에 별도 작업 키를 사용하고 성공·실패·대기 결과를 따로 남깁니다. 이 API에는 일괄 수정 endpoint가 없습니다. + +## 사용자 편집 화면에서 비교 + +사용자 정보 편집 화면에서 `WORKS 정보 비교`를 선택합니다. 저장된 SSO 정보와 현재 WORKS 값을 비교하며, 다른 항목 아래에 `WORKS: 값`을 표시합니다. 이 버튼은 값을 수정하지 않습니다. + +비교 대상은 이름·이메일·전화번호·사번·보조 이메일·대표 소속의 부서·직급·직책·직무입니다. 원격 API가 반환하지 않은 필드는 조회 불가로 표시합니다. 미연결 계정·연동 비대상·조회 실패는 값 불일치와 구분합니다. SSO 보조 전화번호와 계정 상태는 이 필드 비교 대상이 아닙니다. + +화면을 수정하면 이전 비교 결과를 숨깁니다. 변경을 저장한 뒤 다시 비교합니다. WORKS 반영은 비동기 처리이므로 저장 직후에는 이전 WORKS 값이 표시될 수 있습니다. diff --git a/docs/sso_guides/integration-directory-user-create-api-guide.md b/docs/sso_guides/integration-directory-user-create-api-guide.md new file mode 100644 index 0000000..b6010d5 --- /dev/null +++ b/docs/sso_guides/integration-directory-user-create-api-guide.md @@ -0,0 +1,447 @@ +# Directory API 외부 사용자 계정 생성 이용 가이드 + +이 문서는 외부 시스템이 Baron SSO의 Directory API를 사용해 외부 사용자 계정을 한 명씩 생성하는 방법을 설명합니다. API와 필드 이름은 OpenAPI 계약의 영문 표기를 그대로 사용합니다. + +> Staging Swagger UI: [https://sso.hmac.kr/api/docs](https://sso.hmac.kr/api/docs) +> +> Staging OpenAPI: [https://sso.hmac.kr/api/openapi.yaml](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입니다. + +![Staging Swagger UI의 Directory 외부 사용자 생성 API](../snapshots/staging-swagger-directory-user-create-2026-08-26.png) + +Swagger UI의 **Try it out**은 실제 staging 요청을 전송합니다. 승인된 테스트 Tenant와 테스트용 Integration credential을 준비한 경우에만 사용하고, 실제 Secret이나 PII가 캡처·로그·issue에 남지 않도록 주의합니다. + +## 1. 적용 범위 + +사용자 생성 API는 다음 endpoint입니다. + +```http +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 계약](../../backend/docs/openapi.yaml), [Directory handler](../../backend/internal/handler/directory_integration_handler.go), [Directory handler 테스트](../../backend/internal/handler/directory_integration_handler_test.go) + +## 2. 보안 경계와 사전 조건 + +호출 전에 다음 조건을 충족해야 합니다. + +1. 대상 Tenant의 UUID를 확인합니다. `primaryTenantId`는 Tenant slug가 아닌 UUID여야 합니다. +2. 대상 Tenant를 포함하는 `tenant_subtree` scope의 `directory.user.create` credential을 발급합니다. +3. 대상 Tenant는 credential의 scope root 또는 그 descendant여야 합니다. +4. `hanmac-family`와 그 전체 subtree는 Directory 연동 대상이 아닙니다. +5. 요청마다 비어 있지 않은 새 `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](../../backend/internal/domain/integration_credential.go), [Integration credential 인증 middleware](../../backend/internal/middleware/integration_credential_auth.go), [Integration Credential 및 Directory API 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Integration-Credential-%EB%B0%8F-Directory-API-%EC%A0%95%EC%B1%85) + +## 3. Integration credential 발급 + +### 3.1 AdminFront에서 발급 + +fresh identity의 raw role이 exact `super_admin`인 관리자가 다음 순서로 발급합니다. + +1. AdminFront의 `/api-keys`에서 **API 키 관리**를 엽니다. +2. **Directory 연동** 탭을 선택합니다. +3. 이름과 발급 사유를 입력합니다. +4. Operation에서 **외부 사용자 생성**(`directory.user.create`)을 선택합니다. +5. **Scope root Tenant**를 선택합니다. Directory credential의 scope는 항상 `tenant_subtree`입니다. +6. 유효기간을 `30`, `90`, `180`, `369`일 또는 `만료 없음` 중에서 선택합니다. 기본값은 90일입니다. +7. **발급하고 Secret 확인**을 선택한 뒤 `clientId`와 `clientSecret`을 승인된 Secret 저장소에 즉시 저장합니다. + +창을 닫으면 `clientSecret`을 다시 조회할 수 없습니다. 분실했을 때는 기존 Secret을 복구하지 말고 rotate하여 새 Secret으로 교체합니다. + +근거: [AdminFront API 키 관리 화면](../../adminfront/src/features/api-keys/ApiKeyManagementPage.tsx), [AdminFront 관리 API client](../../adminfront/src/lib/adminApi.ts) + +### 3.2 관리 API에서 발급 + +관리 API를 이용할 때도 exact `super_admin`의 interactive access token이 필요합니다. 아래 값은 모두 예시 placeholder이며 실제 token, Secret 또는 운영 Tenant UUID를 문서나 shell history에 저장하지 마십시오. + +```bash +BASE_URL='https://sso.hmac.kr' +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`은 이 응답에서만 한 번 표시됩니다. + +```json +{ + "credential": { + "id": "22222222-2222-4222-8222-222222222222", + "name": "Example HR directory user creator", + "clientId": "bic_", + "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": "" +} +``` + +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 계약](../../backend/docs/openapi.yaml), [Integration credential 관리 handler](../../backend/internal/handler/integration_credential_handler.go), [server route](../../backend/cmd/server/main.go) + +## 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 | `+`와 국가번호를 포함하며 공백·하이픈을 넣지 않습니다. 형식 예: `+` | + +### 4.3 금지 필드 + +허용 목록에 없는 필드는 `400 invalid_request`로 거부됩니다. 특히 다음 필드를 보내지 마십시오. + +- `password` +- `role` +- `status` +- `affiliationType` +- `tenantSlug` +- `metadata` +- `additionalTenantIds` +- `additionalAppointments` +- membership 또는 관계를 직접 표현하는 그 밖의 필드 + +첫 생성에서는 `primaryTenantId` 하나만 지정합니다. 추가 일반 소속은 사용자 생성 성공 후 별도의 `directory.user.memberships.update` credential과 membership PATCH로 처리합니다. + +### 4.4 curl 예시 + +예시는 예약 domain인 `example.test`와 문서용 UUID만 사용합니다. + +```bash +BASE_URL='https://sso.hmac.kr' +BARON_KEY_ID='bic_' +BARON_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`](../../backend/docs/openapi.yaml), [strict JSON 및 입력 검증](../../backend/internal/handler/directory_integration_handler.go) + +## 5. 성공 응답과 idempotency + +성공 시 HTTP `201 Created`와 다음 형태의 응답을 반환합니다. + +```json +{ + "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](../../backend/internal/handler/directory_integration_handler.go), [durable identity mutation 처리](../../backend/internal/identitywrite/service.go), [identity mutation journal](../../backend/internal/repository/identity_mutation_journal_repository.go) + +## 6. 오류 처리 + +오류 body는 기본적으로 `error`, `code`, 선택적 `details`를 사용합니다. + +```json +{ + "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가 포함될 수 있습니다. + +```json +{ + "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](../../backend/docs/openapi.yaml), [Directory 오류 분기](../../backend/internal/handler/directory_integration_handler.go), [Integration credential 인증 오류](../../backend/internal/middleware/integration_credential_auth.go) + +## 7. 생성 후 확인 + +`201` 응답의 `userId`, `email`, `primaryTenantId`, `status`를 우선 보관합니다. Integration credential에는 사용자 관리 조회 권한이 없으므로 최종 identity 확인은 권한이 있는 interactive 관리자 경로로 수행합니다. + +```bash +BASE_URL='https://sso.hmac.kr' +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](../../backend/cmd/server/user_read_routes.go), [관리자 user 조회 handler](../../backend/internal/handler/user_handler.go), [staging live acceptance](../../test/issue_1878_staging_directory_live_e2e.mjs) + +## 8. Password 설정 + +Directory 사용자 생성 요청과 응답에는 password가 없습니다. caller가 초기 password를 지정하거나 읽을 수 없으며, 서버 내부 identity 생성 절차에서 사용하는 password도 외부에 반환하지 않습니다. + +사용자는 기존 password reset 흐름에서 본인의 login identifier로 재설정을 요청하고, 전달받은 일회성 link를 통해 새 password를 직접 설정합니다. email은 생성 필수값이므로 일반적으로 생성한 email을 login ID로 사용합니다. + +```http +POST /api/v1/auth/password/reset/initiate +Content-Type: application/json + +{ + "loginId": "external.user@example.test" +} +``` + +운영 시스템이 reset token 또는 새 password를 대신 수집해서는 안 됩니다. `dryRun`과 `drySend`는 운영에서 사용할 수 있는 일반 연동 옵션이 아닙니다. + +근거: [password reset route](../../backend/cmd/server/main.go), [password reset handler](../../backend/internal/handler/auth_handler.go), [password reset request model](../../backend/internal/domain/auth_models.go), [password 미수신 테스트](../../backend/internal/handler/directory_integration_handler_test.go) + +## 9. 추가 소속은 별도 PATCH와 별도 credential 사용 + +사용자 생성용 `directory.user.create` credential로 membership PATCH를 호출하면 `403 credential_operation_mismatch`가 반환됩니다. 추가 일반 소속에는 `directory.user.memberships.update` 전용 credential을 별도로 발급합니다. + +```http +PATCH /api/v1/integrations/directory/users/{userId}/memberships +``` + +```bash +MEMBERSHIP_KEY_ID='bic_' +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 계약](../../backend/docs/openapi.yaml), [membership handler](../../backend/internal/handler/directory_integration_handler.go), [membership 회귀 테스트](../../backend/internal/handler/directory_integration_handler_test.go) + +## 10. 테스트 cleanup과 credential revoke + +로컬 또는 staging 검증용으로 만든 resource는 검증이 끝난 뒤 생성의 역순으로 정리합니다. + +1. membership 검증을 완료합니다. +2. 생성용과 membership용 credential을 각각 revoke해 추가 호출을 먼저 차단합니다. +3. exact `super_admin` 또는 삭제 권한이 있는 interactive 관리자가 임시 사용자를 삭제합니다. +4. 테스트용 Tenant가 있다면 descendant부터 삭제합니다. +5. 관리자 또는 phone-login 검증 session을 종료합니다. + +임시 사용자 삭제 예시는 다음과 같습니다. 삭제도 별도의 `Idempotency-Key`를 사용합니다. + +```bash +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 예시는 다음과 같습니다. + +```bash +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 구현](../../backend/internal/handler/integration_credential_handler.go), [staging cleanup 구현](../../test/issue_1878_staging_directory_live_e2e.mjs) + +## 11. Staging 검증 사실 + +2026-08-24 staging live acceptance에서 connect phone-login으로 획득한 exact `super_admin` session을 사용해 다음 항목을 검증했습니다. + +- `directory.user.create`와 `directory.user.memberships.update` credential을 분리 발급했습니다. +- 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](../../test/issue_1878_staging_directory_live_e2e.mjs), [staging assertion](../../test/issue_1878_staging_directory_assertions.mjs), [Integration Credential 및 Directory API 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Integration-Credential-%EB%B0%8F-Directory-API-%EC%A0%95%EC%B1%85) + +## 12. 호출자 운영 체크리스트 + +- [ ] `directory.user.create` exact 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](../../backend/docs/openapi.yaml) +- [Directory integration handler](../../backend/internal/handler/directory_integration_handler.go) +- [Directory integration handler 테스트](../../backend/internal/handler/directory_integration_handler_test.go) +- [Integration credential 관리 handler](../../backend/internal/handler/integration_credential_handler.go) +- [Integration credential 인증 middleware](../../backend/internal/middleware/integration_credential_auth.go) +- [Integration credential domain](../../backend/internal/domain/integration_credential.go) +- [Identity write service](../../backend/internal/identitywrite/service.go) +- [Identity mutation journal repository](../../backend/internal/repository/identity_mutation_journal_repository.go) +- [AdminFront API 키 관리 화면](../../adminfront/src/features/api-keys/ApiKeyManagementPage.tsx) +- [Staging Directory live acceptance](../../test/issue_1878_staging_directory_live_e2e.mjs) +- [Integration Credential 및 Directory API 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Integration-Credential-%EB%B0%8F-Directory-API-%EC%A0%95%EC%B1%85) diff --git a/docs/sso_guides/org-context-api.md b/docs/sso_guides/org-context-api.md new file mode 100644 index 0000000..3758606 --- /dev/null +++ b/docs/sso_guides/org-context-api.md @@ -0,0 +1,44 @@ +# Org Context API Guide + +## 목적 + +외부 조직도·Directory consumer가 Baron의 Tenant subtree와 최소 사용자 projection을 조회하는 계약이다. 자체적인 사용자 연결 기준을 email이나 표시 이름으로 만들지 않는다. + +## 호출 + +- Endpoint: `GET /api/v1/integrations/org-context` +- OperationID: `baron.org-context.read` +- query에 `tenantId` 또는 정책상 허용된 `tenantSlug`를 지정한다. +- credential의 Tenant subtree와 exposure 상한 안에서만 조회한다. + +## 식별자 + +- Tenant와 member의 canonical ID는 UUID다. +- `members[].id`는 Baron canonical identity UUID다. +- email, employee number, name은 표시·검색 속성이며 stable join key가 아니다. +- deprecated email index가 있더라도 merge나 picker selection 기준으로 사용하지 않는다. + +## Payload + +- response의 Tenant hierarchy와 member projection을 함께 versioned contract로 처리한다. +- 알 수 없는 optional field는 무시하되 required identity field 누락은 실패로 처리한다. +- consumer가 임의로 parent relation이나 Membership을 보정해 쓰지 않는다. +- pagination/cursor가 있으면 모든 page를 수집한 뒤 snapshot 경계를 유지한다. + +## 오류와 cache + +- scope 밖 Tenant는 `403`으로 처리한다. +- 존재하지 않는 Tenant와 노출되지 않는 Tenant를 client가 권한 추측에 사용하지 않는다. +- projection/readiness 오류를 빈 조직으로 cache하지 않는다. +- cache key에는 Tenant, exposure, API contract version을 포함한다. + +API로 받은 데이터를 직접 선택 UI에 연결하려면 [조직선택기 통합 가이드](org-picker-integration.md)의 **Integration API 데이터 + Picker SDK 방식**을 따릅니다. RP Backend가 credential을 보관하고, 브라우저는 `buildOrgChartModel()`과 `renderOrgPicker()`만 사용합니다. + +## SDK migration + +- v1의 email 중심 join을 UUID 중심 model로 교체한다. +- picker value, selected member, merge key를 모두 UUID로 통일한다. +- compatibility index는 read-only 보조로만 유지한다. +- v1/v2 혼합 배포 시 payload version을 확인하고 silent fallback하지 않는다. + +Credential 발급과 회수는 [Directory API Guide](directory-api.md), 정확한 response schema는 OpenAPI를 따른다. diff --git a/docs/sso_guides/org-picker-integration.md b/docs/sso_guides/org-picker-integration.md new file mode 100644 index 0000000..e91832d --- /dev/null +++ b/docs/sso_guides/org-picker-integration.md @@ -0,0 +1,315 @@ +# 외부 서비스 조직선택기 통합 가이드 + +외부 서비스에서 조직·사용자를 선택하는 두 가지 연동 방법입니다. 이 문서에서 RP(Relying Party)는 Baron과 연동하는 외부 서비스를 뜻합니다. 현재 저장소 구현을 기준으로 합니다. + +| 필요한 방식 | 데이터 조회 | 선택 UI | 읽을 절 | +| --- | --- | --- | --- | +| Integration API로 받은 데이터를 자체 화면에서 선택 | RP Backend가 Integration Credential로 조회 | 독립 SDK의 `renderOrgPicker()` | 0절 | +| OrgFront 선택기 화면을 그대로 임베딩 | iframe 안의 OrgFront가 조회 | `/embed/picker`와 `postMessage` | 1~5절 | + +## 0. Integration API 데이터 + Picker SDK 방식 + +이 방식은 **RP Backend가 조직 데이터를 조회하고, RP 브라우저는 선택 UI만 렌더링**합니다. OrgFront iframe이나 OrgFront 사용자 로그인 세션을 사용하지 않습니다. RP 자체 로그인과 권한 검사는 계속 필요합니다. + +```text +RP 브라우저 → RP Backend → GET /api/v1/integrations/org-context + │ Integration Credential + └─ 승인된 v2 응답 → buildOrgChartModel → renderOrgPicker +``` + +### Credential과 조회 API + +1. [Directory API 가이드](directory-api.md)의 절차로 AdminFront에서 Integration Credential을 발급합니다. OperationID는 `baron.org-context.read`, 범위는 필요한 Tenant subtree, 사용자 선택에 필요한 exposure는 `members`로 승인받습니다. +2. key ID와 secret을 **RP Backend에 보관**합니다. 브라우저 JavaScript, iframe URL, `VITE_*` 환경변수에 넣지 않습니다. +3. RP Backend가 사용자의 로그인과 요청한 조직에 대한 접근 권한을 확인한 뒤 아래 API를 호출합니다. `tenantId` 또는 `tenantSlug` 중 하나만 전달합니다. + +```http +GET /api/v1/integrations/org-context?tenantId=&exposure=members +X-Baron-Key-ID: +X-Baron-Key-Secret: +Accept: application/json +``` + +응답은 `schemaVersion: "baron.org-context.v2"`이며 `scope`, `tree`, `tenants`를 포함합니다. 원본 계약을 유지한 채 브라우저가 사용할 수 있는 응답으로 전달합니다. 이 예제처럼 사용자 이름을 표시하려면 `members`를 사용합니다. `structure`는 기본 exposure로 조직 구조만 조회하며 사용자 이름을 표시하는 다중선택 예제에는 적합하지 않습니다. 정확한 필드와 오류·cache 처리는 [Org Context API 가이드](org-context-api.md)를 따릅니다. + +Credential은 서비스에 부여된 범위입니다. RP Backend는 이 범위 전체를 모든 RP 사용자에게 공개하지 말고, 해당 사용자가 조회해도 되는 subtree인지 별도로 확인해야 합니다. `401`/`403`/조회 실패를 빈 조직도 응답으로 바꾸지 않습니다. + +SDK의 `createOrgContextClient({ baseUrl, credentials: { keyId, keySecret } })`와 `fetchOrgContext({ tenantId, exposure: "members" })`를 Backend 조회에 사용할 수도 있습니다. 이때도 secret을 사용하는 코드는 Backend에서만 실행합니다. + +### SDK 파일 준비 + +SDK는 저장소에서 ES module과 UMD 파일로 빌드합니다. + +```sh +cd orgfront +pnpm build:org-context-chart +``` + +`orgfront/dist/org-context-chart/`에 `boc-.js`, `boc-.min.js`, UMD 파일 등이 생성됩니다. 필요한 ES module 파일 한 개를 RP의 정적 파일로 배포합니다. 아래 예제는 그 파일을 `/vendor/baron-org-context-chart.js`라는 이름으로 배포했다고 가정합니다. 이 주소는 RP가 정하는 경로이며, Baron의 고정 CDN 주소나 npm 패키지명이 아닙니다. + +### 이미 받은 데이터로 사용자 선택기만 렌더링 + +아래 `/api/org-picker-context`는 **RP가 구현하는 예시 endpoint**입니다. 앞의 인증·권한 검사를 수행한 뒤 Baron v2 응답을 반환하도록 구현합니다. 브라우저는 Integration Credential을 알 필요가 없습니다. + +```html +
+

+```
+
+```js
+import {
+  buildOrgChartModel,
+  renderOrgPicker,
+} from "/vendor/baron-org-context-chart.js";
+
+// RP 사용자 세션으로 자신의 Backend를 호출합니다.
+const response = await fetch("/api/org-picker-context", {
+  credentials: "same-origin",
+  headers: { Accept: "application/json" },
+});
+if (!response.ok) throw new Error(`조직 조회 실패: ${response.status}`);
+const payload = await response.json();
+if (payload.schemaVersion !== "baron.org-context.v2") {
+  throw new Error("지원하지 않는 조직 데이터 버전입니다.");
+}
+
+// 이미 API 응답을 보유하고 있다면 위 fetch 없이 payload부터 사용합니다.
+const model = buildOrgChartModel(payload);
+const host = document.getElementById("api-org-picker");
+const result = document.getElementById("api-org-picker-result");
+let picker;
+picker = renderOrgPicker(host, model, {
+  variant: "orgfront",
+  mode: "multiple",
+  selectable: "user",
+  includeDescendants: true,
+  showDescendantToggle: true,
+  onChange(selection) {
+    result.textContent = `${selection.length}명 선택 중`;
+  },
+  onConfirm(selection) {
+    const userIds = [...new Set(selection
+      .filter(item => item.type === "user")
+      .map(item => item.id))];
+    // 이 UUID들을 RP Backend에 제출합니다. 저장 권한은 Backend가 재검사합니다.
+    result.textContent = JSON.stringify(userIds, null, 2);
+    picker.destroy();
+  },
+  onCancel() {
+    result.textContent = "선택을 취소했습니다.";
+    picker.destroy();
+  },
+});
+// 부모 모달을 닫거나 컴포넌트를 해제할 때도 picker.destroy() 호출
+```
+
+예제는 ES module로 실행합니다. HTML에 직접 넣을 때는 JavaScript 코드를 `
+```
+
+`#rp-consent-config`는 RP가 제공해야 하는 `application/json` 요소입니다. 서버가 JSON을 HTML에 삽입할 때 `<` 등을 이스케이프하여 script 종료 문자열을 데이터가 만들지 못하게 합니다. 허용 continuation은 실제 Hydra/RP callback origin과 exact path에 맞춰 서버·SDK 양쪽에 설정합니다.
+
+### 일반 script 방식
+
+같은 산출물을 JavaScript MIME type으로 호스팅합니다. `.cjs` 확장자도 다운로드 파일로 응답하지 않도록 RP 정적 서버를 설정합니다.
+
+```html
+
+
+```
+
+`locale`은 `ko`·`en`, `theme`은 `light`·`dark`입니다. 최초 load부터 abort와 화면 해제가 연결되고, `view.ready`로 최초 렌더링 작업을 기다릴 수 있습니다. `destroy()` 후에는 늦은 응답이 화면이나 navigation을 변경하지 않습니다. 네트워크 취소가 이미 완료된 서버 승인을 취소한다는 뜻은 아닙니다.
+
+`createFetchTransport.timeoutMs`는 기본 10초이며 caller의 취소 신호와 함께 적용됩니다. Timeout 후 자동 재전송하지 않습니다. 같은 Noto Sans KR·데이터·viewport에서 비교한 [UserFront/SDK 캡처](https://gitea.hmac.kr/baron/baron-sso/issues/2104#issuecomment-68696)를 참고하시면 됩니다. 구조·색상·폭·권한 행 간격을 기준으로 맞추며 Flutter와 DOM의 모든 픽셀이 동일하다는 뜻은 아닙니다.
+
+### RP가 완전히 자체 UI를 만드는 경우
+
+같은 `createConsentClient`에 transport를 연결하고 `await client.load(rpFlowId, signal)` 결과를 직접 렌더링합니다. `kind: "redirect"`이면 검증된 URL로 이동하고 `kind: "consent"`이면 `clientName`과 `scopes`의 `name`·`description`·`mandatory`를 사용합니다. 승인은 `client.accept(rpFlowId, selectedScopes, signal)`, 전체 거절은 `client.reject(rpFlowId, signal)`입니다.
+
+최초 load 이전부터 `AbortController`를 화면 생명주기에 연결합니다. 같은 client의 load 재호출은 표시 모델을 재사용하며 서버에 재전송하지 않습니다. 오류·불명확한 결과나 완료 이후에는 해당 client를 재사용하지 않고 사용자 동작으로 새 authorization부터 시작합니다.
+
+### Same-origin RP adapter
+
+SDK transport는 `POST /auth/consent`에 아래 body와 `X-CSRF-Token`, same-origin cookie를 보냅니다. `/auth/consent`는 Baron endpoint가 아니라 RP가 제공하는 예시 경로입니다.
+
+```json
+{
+  "flowId": "",
+  "operation": "accept",
+  "grant_scope": ["openid", "profile"]
+}
+```
+
+`operation`은 `load`, `accept`, `reject`이며 `grant_scope`는 accept에만 사용합니다. [서버 전용 adapter 예제](../../userfront/consent-sdk/examples/rp-adapter.mjs)를 HTTP handler에 연결할 때 다음 경계를 지킵니다.
+
+- `sessionId`는 RP가 무결성을 검증한 브라우저 로그인 transaction의 HttpOnly cookie에서, `origin`은 실제 요청의 Origin에서, `csrfToken`은 요청 header에서 얻습니다. 아직 로그인 완료된 RP session을 요구하는 뜻은 아닙니다. Body의 동명 필드를 신뢰하지 않습니다.
+- 서버 transaction에는 `sessionId`, `csrfToken`, `expiresAt`, `loginChallenge`, `consentChallenge`, `state: "ready"`와 별도 OIDC state·nonce·PKCE verifier·cookie jar를 저장합니다. Flow ID는 서버의 암호학적 난수로 만들고 해당 브라우저 transaction에만 연결합니다. 만료는 Baron 결속의 10분 이내로 설정합니다.
+- Adapter 설정의 `signAssertion(audience)`는 RP 서버 키로 해당 endpoint용 JWT를 생성합니다. `continuationURLs`에는 허용 origin+exact path를 넣습니다.
+- 예제의 Map과 상태 전이는 단일 프로세스 reference입니다. 다중 worker RP는 공유 transaction 저장소의 원자적 잠금/CAS로 같은 보장을 구현합니다.
+- HTTP handler는 반환 `status`, `headers`, `body`를 그대로 전달하고 request body 크기를 제한합니다. 인증정보나 내부 오류를 추가하지 않습니다.
+- `resolveConsentContinuation`에는 authorization 시작부터 유지한 transaction 전용 cookie jar의 fetch를 주입합니다. 허용 Hydra 경로를 따라 중앙 consent Location만 읽고 화면 자체는 호출하지 않습니다.
+- `completeAuthorization(redirectTo, flow)`에는 `resolveAuthorizationCallback`을 연결합니다. 같은 jar의 fetch, `hydraOrigin`, exact `callbackURL`, 저장한 OIDC `state`를 전달하면 최종 callback을 검증하여 반환합니다. 이 함수는 callback을 직접 fetch하거나 code를 교환하지 않습니다. 브라우저가 RP callback을 호출하면 RP의 기존 OIDC 처리기가 code 교환·nonce 등 token 검증과 session 생성을 완료합니다.
+
+## 오류와 429 처리
+
+### API 응답 검증 순서
+
+RP 자체 UI와 제공 JS UI 모두 다음 순서를 적용합니다. TypeScript 타입만 신뢰하지 않고 수신 JSON을 실행 시점에 검증합니다.
+
+```mermaid
+flowchart TD
+    A[API 응답] --> B{HTTP 성공?}
+    B -- 아니요 --> E[status와 안전한 reason/request_id 확인]
+    E --> F[현재 흐름 중단, 자동 재시도 금지]
+    F --> G{next_action}
+    G -- restart_login --> H[사용자 동작으로 새 authorization]
+    G -- contact_support 또는 미지원 --> I[문의 안내]
+    B -- 예 --> C{redirectTo 필드 있음?}
+    C -- 예 --> J{유효 URL 및 허용 origin/path?}
+    J -- 예 --> K[완료 처리 후 한 번 이동]
+    J -- 아니요 --> L[응답 계약 오류, 이동 금지]
+    C -- 아니요 --> D{load 응답이며 client와 scope가 유효?}
+    D -- 예 --> M[동의 UI 표시]
+    D -- 아니요 --> L
+    M --> N[명시적 승인 또는 거절, 중복 제출 차단]
+    N --> A
+```
+
+| 검증 대상 | 기대값과 실패 처리 |
+| --- | --- |
+| HTTP status와 JSON | 2xx여도 malformed JSON/필수 필드 누락은 성공이 아닙니다. Mutation을 다시 보내지 않습니다. |
+| `redirectTo` | 비어 있지 않은 URL, 허용 scheme/origin/path를 검증합니다. Credential 포함 URL과 임의 외부 redirect를 거부하고 query를 보존합니다. |
+| `client.client_id` | RP가 시작한 client와 일치해야 합니다. 서버는 자동 승인 가능한 조회 전에도 결속을 검증합니다. |
+| `requested_scope` | 문자열 배열로 확인합니다. `offline`은 UserFront 표시 정책에 따라 제외하고 `offline_access`는 유지합니다. |
+| `scope_details` | 표시 설명과 필수 여부를 검증하고 텍스트로 렌더링합니다. 필수 `openid`를 해제하지 않습니다. |
+| 승인 `grant_scope` | 조회한 scope 중 선택 목록과 필수 scope를 사용합니다. 전체 거절은 별도 reject입니다. |
+| `code`/`next_action` | 등록된 코드로 실패를 구분합니다. 알 수 없는 코드·행동도 자동 로그인을 유발하지 않습니다. |
+| `request_id`/`Retry-After` | 응답에 있을 때 문의·대기 안내에 사용합니다. 민감한 원문 응답을 화면에 노출하지 않습니다. |
+| 화면 생명주기 | 최초 load 중에도 해제·취소할 수 있어야 합니다. 해제 후 응답은 렌더링·navigation을 일으키지 않습니다. |
+
+Consent 오류는 [RP 통합 가이드의 오류 처리](rp-integration.md#consent-오류-처리)를 따릅니다. `status`와 `code`를 표시하고, 선택적인 `message`는 번역하지 않는 영문 설명으로 취급합니다.
+`next_action`과 `request_id`는 복구 안내와 문의에 사용합니다. `pkce_restart_required`만으로 PKCE 불일치를 단정하지 않습니다.
+`restart_login`은 사용자 동작으로 새 authorization을 시작하는 뜻입니다. 기존 challenge·state·nonce·PKCE 값을 재사용하지 않습니다.
+
+로그인 자격 판정에서 먼저 거부되면 [공통 consent 접근 검사](../../backend/internal/handler/auth_handler_consent_access.go)의 응답은 `code`로 원인을 구분합니다. `403 tenant_not_allowed`는 RP의 소속 조건 거부, `403 rp_whitelist_denied`는 whitelist 미등록, `503 temporarily_unavailable`은 정책 확인 불가입니다. Tenant 거부를 consent 미승인으로 바꾸어 안내하지 않습니다. 기존 동의가 있어도 로그인 자격 조건은 별도로 충족해야 합니다. 관리자 작업은 [whitelist 가져오기 절차](rp-integration.md#비공개-tenant에서-whitelist-가져오기)를 참고합니다.
+
+429에서는 해당 요청 루프를 중단하고 응답 host, query 없는 path, method, 시각·시간대, status, 요청 ID, 단계별 호출 횟수와 `Retry-After` 유무를 확인합니다. Header가 있다는 보장은 없으며, 대기 시간이 끝나도 consent mutation을 자동 재전송하지 않습니다. Timeout·409·5xx나 거절도 전체 로그인 자동 반복 조건이 아닙니다.
+
+프로덕션 429와 consent 누락의 인과관계는 미확정입니다. 현재 코드에서 Kratos password SignIn upstream 429는 503 `temporarily_unavailable`로 변환되므로 응답 위치부터 구분해야 합니다. Password·token·assertion·cookie·challenge·verifier와 그 prefix, 전체 redirect URL을 로그에 남기지 않습니다. [오류 처리 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Error-Handling-Policy)을 따릅니다.
+
+상세 개발 범위, RED/GREEN·실제 RP E2E 기준과 검증 증거는 [#2104](https://gitea.hmac.kr/baron/baron-sso/issues/2104)에서 관리합니다.
diff --git a/docs/sso_guides/rp-installed.md b/docs/sso_guides/rp-installed.md
new file mode 100644
index 0000000..3df9b4c
--- /dev/null
+++ b/docs/sso_guides/rp-installed.md
@@ -0,0 +1,71 @@
+# 설치형 RP 가이드
+
+## 보안 모델
+
+설치형 Interactive RP는 Public PKCE client다. 앱은 Hydra client secret이나 Hydra token을 보관하지 않는다. 복구 기능을 선택한 앱은 기기 키에 결합된 Baron 복구 자격증명을 OS 보안 저장소에 보관한다. [복구 및 다중 프로세스 연동](rp-integration.md#설치형-앱의-세션-복구와-다중-프로세스)을 따른다.
+
+- Installation ID: RP·환경·local enrollment별 256-bit random value
+- Device Key: 설치별 비대칭 key pair
+- Installation Binding: 서버가 Installation ID를 RP와 subject에 결속한 값
+- Device Session: Baron Token Vault를 가리키는 opaque session
+
+MAC address, IMEI, serial, hostname, IP, User-Agent를 Installation ID로 사용하지 않는다. private key는 OS secure storage에 보관하고 다른 환경이나 RP와 공유하지 않는다.
+
+## 최초 등록
+
+1. 앱이 새 Installation ID와 device key pair를 만든다.
+2. 일반 Public PKCE authorization과 consent를 완료한다.
+3. 앱이 authorization completion, Installation ID, public JWK, device proof를 Backend Broker에 제출한다.
+4. Backend가 subject, RP, proof, quota, key tombstone을 검증한다.
+5. Backend는 Hydra token을 Token Vault에 보관하고 opaque device session을 반환한다. RP별 발급 허용과 앱 opt-in이 켜져 있고 서버 전체 차단이 없으면 복구 자격증명도 반환한다.
+
+Device public JWK를 DevFront Client JWKS에 등록하지 않는다.
+
+### Device Proof 시간 오류 처리
+
+이 절은 저장소 코드의 응답을 설명합니다. 오류 정책은 [Error Handling Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/Error-Handling-Policy)에서 관리합니다.
+구현과 검증 기록은 [#2186](https://gitea.hmac.kr/baron/baron-sso/issues/2186)을 참고하세요. 운영 반영 여부는 별도로 확인하세요.
+
+Authorization completion의 시간 오류는 HTTP 400과 본문의 `status=400`을 반환합니다. 필수 `code`는 `device_proof_expired`처럼 서버가 확인한 실패를 식별합니다.
+응답 구조와 코드 목록은 [OpenAPI의 InstalledDeviceProofTimeFailureResponse](../../backend/docs/openapi.yaml)를 따릅니다. `message`는 선택적인 영문 설명이며 번역하지 않습니다.
+
+RP는 오류 상세에 HTTP 상태와 `code`를 표시합니다. `message`가 있으면 영문 그대로 표시하며, 문구를 해석해서 복구 동작을 선택하지 않습니다.
+`next_action=restart_login`이면 앱의 시각과 proof 생성 시점을 확인한 뒤 새 로그인을 시작합니다. 새 로그인에서는 authorization code와 proof를 새로 만듭니다.
+`next_action=contact_support`이면 `request_id`를 첨부해 관리자에게 문의합니다.
+
+시간 진단의 `retryable`은 `false`입니다. 알 수 없는 코드나 다음 행동을 받아도 같은 proof 또는 authorization code를 자동 재전송하지 않습니다.
+만료와 미래 발급 시각은 서버가 확인한 시간 조건입니다. 이 조건만으로 실제 기기 시계의 방향이나 네트워크 지연을 확정하지 않습니다.
+
+공개 진단과 민감정보의 경계는 [Error Handling Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/Error-Handling-Policy)를 따릅니다.
+
+## Operation 호출
+
+- operation마다 현재 device key로 proof를 서명한다.
+- proof에는 method, path, body digest, nonce/issued time처럼 Backend가 요구하는 context를 결속한다.
+- idempotency key는 동일한 logical operation에 재사용하고 다른 payload에 재사용하지 않는다.
+- timeout 뒤 임의로 새 operation을 만들기 전에 기존 operation 상태를 조회한다.
+- Redis replay 방지는 보조 계층이며 durable 중복 실행 방지는 PostgreSQL execution ledger가 담당한다.
+
+## Device lifecycle
+
+- 등록 상태와 활성 authorization/token family를 구분한다.
+- disable은 새 operation을 막고 local family/session을 fence한다.
+- revoke는 복구 가능한 disable보다 강한 종료다.
+- key rotation은 기존 key proof와 새 key possession을 모두 검증한다.
+- retired/revoked thumbprint는 tombstone으로 보존하고 재사용하지 않는다.
+- rotation 뒤에는 새 key로 fresh PKCE authorization이 필요할 수 있다.
+
+## Quota
+
+- 사용자 device quota는 `(client_id, subject_kind, subject_id)` 경계에서 계산한다.
+- 활성 수는 PostgreSQL token family 상태를 기준으로 한다.
+- Redis TTL이나 마지막 heartbeat만으로 active quota를 계산하지 않는다.
+- quota 퇴출은 local fence와 revoke outbox를 하나의 transaction에 기록한다.
+
+## 운영 확인
+
+- DevFront inventory에서 device ID, subject UUID, Tenant, 등록 상태, 활성 authorization, key generation을 구분해 확인한다.
+- private JWK, Hydra token, raw vault material은 어떤 화면에도 표시하지 않는다.
+- disable/rotate/revoke 뒤 기존 proof와 session이 실패하는지 확인한다.
+
+공통 Client 등록은 [RP Integration Guide](rp-integration.md), 정확한 endpoint와 proof schema는 OpenAPI를 따른다.
diff --git a/docs/sso_guides/rp-integration.md b/docs/sso_guides/rp-integration.md
new file mode 100644
index 0000000..da58dd3
--- /dev/null
+++ b/docs/sso_guides/rp-integration.md
@@ -0,0 +1,437 @@
+# RP 통합 가이드
+
+이 문서는 RP 개발자가 **서비스 목적 → RP 유형 → 필요한 옵션 → RP 구현 → Baron 설정 → 동작 확인** 순서로 연동을 준비하는 가이드입니다. 먼저 아래에서 유형과 옵션 조합을 고르고, 이어지는 화면별 절차를 수행해 주세요. 지원 범위는 현재 저장소 코드 기준이며 운영 배포 여부와는 별도입니다.
+
+조직·사용자 선택기를 RP 화면에 넣으려면 [외부 서비스 조직선택기 통합 가이드](org-picker-integration.md)를 참고하세요.
+
+## 1. 목적에 따라 RP 유형 선택
+
+```mermaid
+flowchart TD
+    A["어떤 주체가 서비스를 사용하나요?"] --> B{"사용자가 로그인하나요?"}
+    B -->|아니요| M["Central M2M: 중앙 서버의 무인 작업"]
+    B -->|예| C{"설치별 키와 Baron Token Vault로
장비를 관리해야 하나요?"} + C -->|예| I["Installed Interactive"] + C -->|아니요| D{"RP Backend를 운영하나요?"} + D -->|아니요| P["Browser/Mobile PKCE"] + D -->|예| E{"로그인·동의 UI를 RP가 제어해야 하나요?"} + E -->|아니요| W["Server-side Web: 중앙 UserFront 사용"] + E -->|예| H["Server-side Headless: 자체 UI 또는 Baron JS UI"] + W --> O["2절에서 필요한 옵션과 지원 조합 확인"] + H --> O + P --> O + I --> O + M --> O +``` + +설치 앱이라는 이유만으로 Installed Interactive를 선택하지는 않습니다. **장비별 키·등록/폐기·중앙 token 보관이 필요하면 Installed**, 앱이 직접 token을 관리하는 일반 로그인이라면 Browser/Mobile PKCE를 검토합니다. 무인 프로그램도 사용자 PC에 비밀키를 배포하는 구조라면 중앙 workload용 M2M과 같은 것으로 취급하지 마세요. + +| 만들려는 서비스 | 선택할 유형·등록 preset | Baron에서 설정할 내용 | RP 측에서 개발·지원할 내용 | 완료 확인 | +| --- | --- | --- | --- | --- | +| 중앙 로그인 화면을 이용하는 사내 웹·포털·업무 서비스 | **Server-side Web** · `interactive_web_server` | Interactive, 기본 인증 `client_secret_basic`, callback URI, scope | Backend의 로그인 시작·callback, state/nonce/PKCE 검증, code 교환·token 검증, 서버 token 보관, RP local session 생성·종료, secret 교체 | 정상·거절 callback을 처리하고 다른 state를 거부하며 서버 session으로 서비스를 이용할 수 있습니다. | +| 자체 화면으로 로그인·동의를 제공하는 웹 서비스 | **Server-side Headless** · `interactive_web_headless` | Interactive, Headless 활성, `private_key_jwt`, HTTPS JWKS, callback·scope | Web 공통 구현 + 서버 private key 보관·assertion 서명·키 교체, 로그인 transaction과 cookie 문맥 유지, same-origin adapter·CSRF 방어, 자체 UI 또는 Baron Consent JS UI 연결 | 로그인→동의→callback 전체가 같은 transaction에 연결되고 잘못된 assertion·다른 transaction 요청은 거부됩니다. | +| Backend 없이 브라우저·모바일 앱에서 일반 OIDC 로그인 | **Browser/Mobile PKCE** · `interactive_web_pkce` | Interactive, Public PKCE, 인증 `none`, callback·scope | 앱의 state/nonce·PKCE S256 생성·검증, code 교환·token 검증, 실행 환경에 맞는 token 저장·폐기, 로그인 재시작 | secret 없이 정상 로그인하고 다른 verifier·state를 거부합니다. 앱이 token 보관 책임을 집니다. | +| Desktop·CLI·local agent를 설치별로 등록하고 폐기하며 token을 중앙에 보관 | **Installed Interactive** · `interactive_installed_broker` | Interactive, Public PKCE, `device_bound_singleflight`, 사용자별 등록 device-key·활성 Token Vault 한도 | Installation ID·device key 생성과 OS secure storage, PKCE 완료를 Broker에 제출, Device Proof 서명, opaque session 사용, operation 상태 조회·멱등 재시도, disable/rotate/revoke 처리 | 앱이 Hydra access/refresh token을 받지 않으며, 폐기된 장비의 기존 proof·session은 실패합니다. | +| 사용자 로그인 없는 중앙 배치·서버 간 API 호출 | **Central M2M** · `central_service_m2m` | M2M, 중앙 workload, `private_key_jwt`, HTTPS JWKS, 필요한 scope | 중앙 private key 보관·assertion 서명, `client_credentials` token 획득·만료 후 재획득, API 호출 권한 확인·키 교체 | 사용자 로그인·consent 없이 허용된 scope로만 호출하고 잘못된 키·만료 assertion은 거부됩니다. | + +기본 등록값은 [RP 생성 preset](../../devfront/src/features/clients/clientIntegrationProfile.ts)을 따릅니다. Interactive와 M2M은 별도 Client로 등록하며 유형 변경이 필요하면 새 `client_id`를 만듭니다. Installed의 device public JWK는 Headless·M2M의 Client JWKS와 용도가 다릅니다. + +## 2. 원하는 옵션과 RP 구현 연결 + +### 유형별 조합을 먼저 확인 + +아래의 `가능`은 설정만 하면 완료된다는 뜻이 아니라 **해당 행의 RP 구현과 서버 선행조건을 충족한 뒤 사용할 수 있음**을 뜻합니다. `현재 제한`은 아래의 추가 로그인 보안 저장 경로에서 거부되는 조합입니다. 중앙 Tenant·계정 정책이나 별도의 장비 통제를 지원하지 않는다는 뜻은 아닙니다. + +| 목적·옵션 | Server-side Web | Headless | Browser/Mobile PKCE | Installed Interactive | Central M2M | +| --- | --- | --- | --- | --- | --- | +| 중앙 화면으로 사용자 로그인 | 기본 | 자체 화면이 필요 없으면 Web 검토 | 기본 | 등록 로그인에 사용 | 해당 없음 | +| 로그인·동의 화면을 RP가 제어 | Headless 선택 | 가능: UI·adapter 필요 | 이 가이드의 Headless 대상 아님 | 이 가이드의 Headless 대상 아님 | 해당 없음 | +| Tenant 단독 제한 | 가능: OIDC 로그인 연동 | 가능: 기존 로그인 연동 | 가능: OIDC PKCE 연동 | 가능: 기존 등록 로그인 연동 | 사용자 로그인 옵션 대상 아님 | +| whitelist 단독 사용 | 가능: logout 수신·세션 종료 필요 | 가능: logout 수신·세션 종료 필요 | HTTPS 수신 서버와 실제 세션 종료 구현 필요 | HTTPS 수신·세션 종료와 기존 Broker 계약을 함께 확인 | 사용자 로그인 옵션 대상 아님 | +| AAL2 강제·Fresh TOTP | 가능: 중앙 검증·logout 구현 필요 | 가능: 서명 introspection·logout 구현 필요 | 현재 제한 | 현재 제한 | 사용자 로그인 옵션 대상 아님 | +| AAL2 권장만 설정 | 가능 | 가능 | 가능 | 가능 | 사용자 로그인 옵션 대상 아님 | +| 사용자 로그인 유지·갱신 | RP Backend가 refresh 처리 | RP Backend가 refresh 처리 | 앱이 refresh 처리 | Baron Vault·Broker 사용 | 만료 후 client credentials로 재획득 | +| 설치별 장비 등록·키 회전·폐기 | 해당 없음 | 해당 없음 | 필요하면 Installed 검토 | 전용 기능 | 사용자 장비 기능 대상 아님 | + +whitelist만 사용하면 중앙 token 검증과 introspection용 Client 인증 방식은 필수 조건이 아닙니다. `token_endpoint_auth_method=none`인 RP도 유효한 HTTPS Back-Channel Logout URI와 subject 전체 세션 종료를 지원하면 사용할 수 있습니다. 브라우저나 모바일 앱만 있고 서버 수신 endpoint가 없는 구성은 이 조건을 충족하지 않습니다. `backchannel_logout_session_required`는 `false`로 설정합니다. + +AAL2 강제·Fresh TOTP의 기존 조건은 유지합니다. 이 옵션은 `client_secret_basic/post` 또는 실제 등록 인증 방식이 `private_key_jwt`인 활성 Headless RP에서 사용할 수 있습니다. Headless metadata만 `private_key_jwt`이고 실제 Hydra 등록이 `none`인 구성은 이 조건을 충족하지 않습니다. 해당 Headless RP는 등록된 HTTPS JWKS를 이용한 서명 introspection과 Back-Channel Logout을 구현해야 합니다. 기존 token·RP 자체 세션의 종료 범위는 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow#로그인-보안-재설계-정책-1978)를 따릅니다. + +### 옵션별 구현·설정·확인표 + +| 하고 싶은 일 | RP에서 먼저 구현·지원할 내용 | Baron 설정 순서 | 확인할 결과 | +| --- | --- | --- | --- | +| 특정 회사·테넌트 사용자만 로그인 | OIDC 로그인 성공·거절을 처리합니다. RP 내부 데이터 권한은 별도로 검사합니다. | `로그인 보안`에서 Tenant 트리 선택 → `Tenant 제한 저장` | 대상 소속은 허용되고 범위 밖 계정은 거절됩니다. 선택 후보에 보인다는 이유만으로 그 계정에 로그인 자격이 생기지 않습니다. | +| 허용 Tenant 안에서도 지정 인원만 로그인 | Back-Channel Logout을 실제 세션 종료에 연결합니다. whitelist 거절 시 RP session을 만들지 않습니다. | Tenant 정책 먼저 저장 → HTTPS logout URI 등록·subject 전체 종료 지원 확인 → 명단 구성 → whitelist 활성화. private 원본 가져오기는 정책 저장 후 실행 | 명단 밖 일반 계정은 로그인할 수 없습니다. 제거 후에는 별도로 로그아웃 전달 결과를 확인합니다. | +| 로그인에 AAL2를 강제 | 추가 인증 완료 전 RP session을 만들지 않고 callback 오류·새 로그인 시작을 처리합니다. TOTP secret·내부 evidence를 RP에 복제하지 않습니다. | `RP AAL 정책 모드 = AAL2 강제`, `RP 요구 AAL = AAL2` → 공통 지원 항목 확인 → 저장 | 추가 인증 완료 전 서비스에 진입하지 못하고 완료 후 로그인됩니다. | +| AAL2를 권장하되 RP가 강제하지 않음 | 권장과 강제의 결과를 구분하고 중앙 로그인 흐름을 완료합니다. | `AAL2 권장`, 요구 `AAL2` → 저장 | RP 권장을 강제 차단으로 바꾸지 않습니다. 중앙의 다른 필수 인증 요구는 계속 적용됩니다. | +| 민감 기능에 최근 TOTP 확인 요구 | 중앙이 반환한 재인증 요구를 처리하고 검증 미완료 시 보호 기능을 실행하지 않습니다. RP 내부 중요 command의 인가도 별도로 구현합니다. | 지원 조합 확인 → `민감 기능에 Fresh TOTP 요구` → 공통 지원 항목 확인 → 저장 | 일반 AAL2만 충족한 상태와 최근 TOTP를 확인한 상태를 구분합니다. 이 설정이 RP의 모든 내부 API를 자동 보호하지는 않습니다. | +| 로그인 유지·refresh token 사용 | Web·Headless는 서버, PKCE는 앱에서 갱신과 갱신 거절 처리를 구현합니다. Installed는 Broker operation과 Vault 계약을 사용합니다. | 허용 scope에 `offline_access` → 실제 요청 scope·consent·`refresh_token` grant 확인 | 정상 갱신되고 폐기·거절 시 기존 local authorization을 계속 사용하지 않습니다. M2M에는 이 사용자 갱신 절차를 적용하지 않습니다. | +| 서비스에 필요한 사용자 값 전달 | 필요한 claim만 소비하고 자료형·누락값을 처리합니다. claim을 RP 내부 권한 원장으로 대신 사용하지 않습니다. | `설정`에서 Scope·claim 정의 저장 → 필요 시 `Consents & Claims`에서 사용자값 별도 저장 → 실제 scope 요청 | 승인 범위에 맞는 claim만 최종 결과에 포함됩니다. | +| UserFront에서 앱을 눌러 바로 로그인 시작 | RP 시작 URL에서 새 state·nonce·PKCE를 생성하여 authorization을 시작합니다. | `설정 > 자동 로그인` 활성 → RP 시작 URL 저장 | 기존 중앙 session이 있어도 callback 검증을 거쳐 RP session이 만들어집니다. | +| 중앙 연결 해제 시 RP에서도 로그아웃 | 아래 logout 수신 계약과 local session 종료를 구현합니다. | Back-Channel Logout URI 등록 → SID 필요 여부와 subject-only 전파 호환 확인 | 서명 검증 후 해당 사용자의 local session을 끝내고 중복 요청도 안전하게 처리합니다. | +| 장비 수 제한·분실 장비 차단 | Installed의 key/session 생명주기와 operation 거부 처리를 구현합니다. | Installed 선택 → 등록 device-key·활성 Vault 한도 설정 → inventory에서 관리 | 한도 초과·disable·revoke 후 기대한 등록·operation 차단이 적용됩니다. | + +### 추가 로그인 보안을 위한 RP 공통 구현 계약 + +이 표는 whitelist·AAL2 강제·Fresh TOTP를 사용하는 RP의 구현 항목입니다. 중앙 token 검증은 whitelist 단독 사용에서 선택 사항이며, 나머지 두 옵션의 기존 요건은 유지합니다. Tenant 단독 제한의 설정은 [Tenant 선택과 정책 저장](#tenant-선택과-정책-저장)을 따릅니다. + +| RP 구현 항목 | 구현 내용 | 완료 확인 | +| --- | --- | --- | +| 중앙 token 검증 | 사용하는 경우 Baron의 `/oauth2/introspect` 결과를 접근 판단에 반영합니다. JWT 서명·만료 확인만으로 중앙 정책 변경을 감지했다고 간주하지 않습니다. | 검증 장애를 허용으로 처리하지 않습니다. whitelist 단독 사용에서 이 기능을 생략하면 로그아웃 통지 처리 전 접근이 남을 수 있습니다. | +| Logout 수신 endpoint | HTTPS POST에서 form의 `logout_token`을 수신합니다. `/api/v1/auth/backchannel/jwks.json`으로 signature, issuer, audience, 시간·events·jti 등을 검증하고 RP local session을 종료합니다. | 잘못된 서명·audience를 거부하고 같은 `jti` 재전송과 이미 종료된 session을 안전하게 처리합니다. | +| Session 연결 | Baron subject와 RP local session의 관계를 보관합니다. 현재 subject-only logout에는 `sid`가 없으므로 `sub`로 대상 session을 찾는 경로가 필요합니다. | 중앙 연결 해제 성공과 RP local session 종료를 각각 확인합니다. | +| 오류·재인증 | 로그인·검증·갱신 거절 시 local authorization을 중단하고 상황에 맞게 새 로그인 또는 문의를 안내합니다. | 실패 응답을 성공으로 바꾸지 않고, 새 로그인에는 새 state·nonce·PKCE를 사용합니다. | + +정확한 introspection 요청·인증 계약은 [OpenAPI](../../backend/docs/openapi.yaml)의 `/oauth2/introspect`, logout 구현은 [RP Session and Logout](rp-session-and-logout.md#back-channel-logout), Headless adapter는 [Headless 가이드](rp-headless-consent.md), 장비 구현은 [Installed 가이드](rp-installed.md)를 따릅니다. + +## 3. 선택한 조합을 구현하고 설정하는 순서 + +```mermaid +flowchart TD + A["서비스 목적과 필요한 옵션 목록 작성"] --> B["유형 선택표와 옵션 조합표 대조"] + B --> C{"현재 지원 조합인가요?"} + C -->|아니요| X["목적에 맞는 지원 유형으로 재설계하거나
조합 지원 개발을 별도로 검토"] + C -->|예| D["기본 Client 등록
client_id·callback·scope·키 설정"] + D --> E["유형별 RP 로그인 또는 M2M 기본 흐름 구현"] + E --> F["선택 옵션의 RP 선행 구현
검증·logout·adapter·device proof 등"] + F --> G["RP endpoint·키 배치 후 Baron 설정 저장"] + G --> J{"지원되는 추가 로그인 보안 옵션을
사용하나요?"} + J -->|예| H["선택한 옵션만 저장
whitelist는 Tenant 저장·명단 구성이 선행"] + J -->|아니요| I + H --> I["정상·거절·재인증·로그아웃 확인
설정 새로고침으로 저장값 확인"] +``` + +M2M과 추가 로그인 보안을 사용하지 않는 RP는 옵션 저장 단계를 건너뜁니다. AAL2 권장만 선택했다면 Tenant·명단을 만들 필요가 없습니다. 기본 Client 등록은 구현에 필요한 식별자를 확보하는 단계이며, 옵션 활성화는 RP 선행 구현을 완료한 다음에 진행합니다. + +예를 들어 한맥가족 계열사 중 지정 인원만 이용하는 업무 웹은 기본 OIDC 로그인과 logout 수신을 먼저 구현합니다. Tenant 트리를 저장하고 명단을 구성한 뒤 whitelist를 활성화합니다. whitelist만 사용하면 중앙 검증 지원 체크는 필수가 아닙니다. 자체 로그인 화면이 필요한 Headless도 같은 logout 수신 계약을 구현합니다. 분실 장비의 접근을 끊는 Installed의 Device Key·Broker operation·inventory 계약은 별도로 확인합니다. + +## DevFront 등록 + +1. RP 유형과 실행 위치를 먼저 선택한다. +2. redirect URI를 exact value로 등록한다. +3. Server-side Web은 client authentication과 인증정보를 확인한다. Headless·M2M의 `private_key_jwt`에는 HTTPS JWKS URI를 설정한다. +4. PKCE client이면 public client 경계와 허용 method를 확인한다. +5. scope, consent 설명, 소유 Tenant, 접근 relation을 설정한다. +6. 저장 후 공개 metadata와 실제 authorization request를 대조한다. + +### 연동 앱 목록 탐색 + +DevFront의 연동 앱 목록에서 이름 또는 ID를 입력하여 앱을 검색합니다. 검색창 옆의 고급 필터에서 유형과 상태를 선택합니다. + +상단 제목에서 전체 검색·필터 결과 수를 확인합니다. 앱 이름은 일부 입력이나 오타로 검색할 수 있으며 ID는 전체 값을 입력합니다. 활성 사용자 수로 정렬하려면 검색어를 지우고 `활성 사용자` 열 제목을 누릅니다. 같은 열 제목을 다시 누르면 정렬 방향이 바뀝니다. 검색어를 입력하면 관련도 순서로 표시하고, 지우면 검색 전 정렬로 돌아갑니다. 수치의 의미와 표시 권한은 [UI Design Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/UI-Design-Policy#연동-앱-목록의-활성-사용자-수)를 따릅니다. + +표가 화면의 가용 폭에 들어가면 페이지를 스크롤하고, 표 헤더는 앱 헤더 아래에 유지됩니다. 표가 가용 폭을 넘으면 표 영역 안에서 가로로 스크롤합니다. 이때 목록이 길면 같은 영역 안에서 세로로 스크롤합니다. 관계, 기기, 감사와 같은 긴 표의 탐색 방법은 [긴 목록 탐색](adminfront.md#긴-목록-탐색)을 참고해 주세요. + +### 등록 후 추가 설정 확인 + +RP를 만든 뒤 상세 화면에서 다음 항목을 확인해 주세요. RP 유형과 현재 계정의 권한에 따라 표시되는 항목이 다릅니다. + +| 화면·항목 | 설정 및 확인할 내용 | +| --- | --- | +| 연동 설정 | 발급된 `client_id`, discovery·authorization·token endpoint와 RP 유형별 연동 예제를 확인합니다. 생성 후 로그인 callback의 Redirect URI도 이 화면에서 수정합니다. 유형을 변경해야 하면 새 Client로 등록합니다. | +| 설정: 기본 정보 | 이름·설명·로고 등 사용자에게 표시될 정보와 소유 Tenant를 확인합니다. 소유 Tenant 지정만으로 로그인 대상이나 운영 권한이 자동 설정된다고 간주하지 않습니다. | +| 설정: URI | Back-Channel Logout URI와 SID Claim Required 설정을 확인합니다. 이 URI는 브라우저 이동 주소가 아니라 RP 서버의 logout token 수신 endpoint입니다. 현재 subject-only logout 전파와 SID 지원 범위는 [로그아웃 가이드](rp-session-and-logout.md#특정-rp-연결-해제)를 확인합니다. | +| 설정: 보안 | RP 유형에 맞는 Client 인증 방식, PKCE, secret 또는 HTTPS JWKS를 확인합니다. Public PKCE Client에 secret을 넣거나, Headless·M2M의 인증 방식을 로그인 보안 저장을 통과시키기 위해 임의 변경하지 않습니다. | +| 설정: Scope·Claims | 요청할 scope와 전달할 claim의 이름·값·자료형·대상 및 조건을 확인합니다. 동의가 필요한 정보는 실제 consent 화면과 token 결과까지 대조합니다. | +| Consents & Claims | 동의 요청 항목과 claim 할당을 확인합니다. 로그인 whitelist 등록과 개인정보 제공 동의는 별도 작업입니다. | +| 설정: 자동 로그인 | 사용할 경우 활성화하고 RP의 로그인 시작 URL을 지정합니다. 이 URL에서 RP가 `state`, `nonce`, PKCE를 생성하여 새 authorization 흐름을 시작해야 합니다. | +| 설정: 테넌트 접근 제한 | 기존 metadata 기반 제한입니다. 아래 `로그인 보안 > Tenant 제한`과 저장 위치가 다르며 자동 동기화되지 않습니다. 기존 제한이 남아 있으면 함께 확인합니다. | +| 로그인 보안 | 허용 Tenant, whitelist, AAL 모드·수준, Fresh TOTP, 중앙 검증·로그아웃 지원을 아래 순서로 설정합니다. | +| 관계 | 조회·설정 편집·whitelist 관리 등 실제 작업에 필요한 RP 관계를 부여합니다. 화면 표시와 서버의 권한 검사를 각각 확인합니다. | +| Device inventory | 설치형 RP에서 권한이 있는 경우 등록 장비와 상태를 확인합니다. 장비 등록·폐기 및 M2M 구성은 유형별 절차를 따릅니다. | + +설정 화면의 저장, 로그인 보안의 `정책 저장`, whitelist 멤버 추가·제거는 별도 요청입니다. 한 화면의 저장 성공으로 다른 설정까지 반영되었다고 판단하지 마세요. 기존 metadata 제한과 새 로그인 보안 제한이 모두 설정되어 있으면 로그인 시 두 조건을 각각 충족해야 합니다. 정확한 판정 구현은 [RP 접근 검사](../../backend/internal/loginpolicy/rp_access.go)를 참고해 주세요. + +### 유형별 후속 설정 + +- **Refresh token**: `offline_access`를 허용 scope에 등록하는 것만으로 발급되지 않습니다. 실제 authorization 요청의 scope, consent 승인, `refresh_token` grant 설정을 함께 확인합니다. +- **Custom Claims**: `설정`에서 claim 정의를 저장한 뒤 `Consents & Claims`에서 필요한 사용자별 claim 값을 별도로 설정·저장합니다. 최종 token에 의도한 값만 전달되는지 확인합니다. +- **Secret 관리**: 연동 설정의 secret 재발급은 기존 secret을 즉시 무효화합니다. RP의 인증정보 교체 준비 후 수행하고 새 secret을 안전하게 보관합니다. +- **Headless**: HTTPS JWKS와 키를 구성한 뒤 화면의 JWKS 캐시 상태를 확인합니다. 키 변경 시 `Refresh`로 갱신하거나 필요한 경우 `Revoke Cache`를 사용하고 실제 client assertion을 검증합니다. 세부 절차는 [Headless RP Consent](rp-headless-consent.md)를 참고합니다. +- **Installed Interactive**: 등록 device-key 수와 활성 Token Vault 수의 한도를 확인하고 `Device inventory`에서 장비 상태를 관리합니다. Device Proof·등록·폐기 절차는 [설치형 RP 가이드](rp-installed.md)를 따릅니다. +- **Central M2M**: 중앙 workload에서 `private_key_jwt`와 HTTPS JWKS를 사용합니다. 사용자 consent나 설치형 사용자 장비 흐름과 혼합하지 않으며, 화면에 노출되지 않는 machine 설정을 임의 입력하는 절차로 대체하지 않습니다. + +### 날짜·시간 Custom Claim 편집 + +날짜·시간 Claim을 변경하지 않으면 다른 설정을 저장해도 원본 값과 날짜 설정을 보존합니다. 기본값은 RP의 `설정`에서, 사용자별 값은 `Consents & Claims`의 Claim 편집 화면에서 변경해 주세요. 값을 입력하기 전에 해당 Claim의 시간대 선택 값을 확인해 주세요. 잘못된 날짜와 일광 절약 시간 전환으로 존재하지 않는 현지 시각은 저장할 수 없습니다. 같은 현지 시각이 두 번 나타나면 입력란 아래의 발생 순서 선택에서 `첫 번째 발생` 또는 `두 번째 발생`을 선택해 주세요. + +저장 단위와 날짜 경계는 [Date and Time Standard의 RP custom claim 예외](https://gitea.hmac.kr/baron/baron-sso/wiki/Date-and-Time-Standard)를 따릅니다. 저장 후 같은 편집 화면을 다시 열어 입력한 날짜·시각과 선택한 시간대가 유지되는지 확인해 주세요. + +### NAVER WORKS 사용자 식별자 사용 + +NAVER WORKS 사용자 식별자가 필요하면 authorization 요청에 `profile`을 포함하세요. 이 필드에 별도 Custom Claims 정의를 등록하지 마세요. 동의 후 ID Token 또는 UserInfo에서 `naverworks_user_id`를 읽으세요. 필드가 없으면 NAVER WORKS 식별자 연결을 생략하세요. + +현재 소속 판정은 Kratos `traits`를 사용합니다. `metadata_admin`에만 있는 추가 소속은 [#2219](https://gitea.hmac.kr/baron/baron-sso/issues/2219)에서 추적합니다. + +제공 조건은 [메타데이터 스키마 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Metadata-Schema-Design.-.-#naver-works-사용자-식별자)을 따릅니다. 구현은 [RP claim 구성](../../backend/internal/handler/auth_handler_consent_private.go)을 참고하세요. + +## RP 로그인 보안 + +- 보안설정은 접근 대상, 인증 요구사항, RP 연동 지원 확인으로 구분합니다. 기능 사용은 토글, RP 지원 여부 확인은 체크박스, AAL 모드·수준은 선택 상자로 편집하며 **정책 저장** 후 적용됩니다. +- 허용 테넌트는 이름 라벨로 표시합니다. **허용 Tenant 추가**로 선택하고 라벨의 × 버튼으로 하나씩 제거한 뒤 **정책 저장**을 누릅니다. 라벨에 포인터를 올리면 ID를 확인할 수 있으며, 현재 후보 목록에 없는 기존 선택도 ID와 함께 보존됩니다. 허용 테넌트가 없으면 화이트리스트를 켠 상태로 저장할 수 없습니다. 테넌트 선택을 비우고 저장하면 테넌트 제한이 해제됩니다. +- DevFront의 Client 로그인 보안 화면에서 허용 Tenant, 사용자 whitelist, Required AAL, Fresh TOTP 요구를 관리한다. whitelist는 허용 Tenant가 없으면 활성화할 수 없다. +- RP `config_editor`는 로그인 보안 정책을 조회·수정할 수 있고, RP `manage` 권한은 정책과 whitelist 멤버를 함께 관리한다. 장비·비밀·감사 전용 조회 권한에는 whitelist가 노출되지 않는다. +- whitelist는 별도 Tenant나 AdminFront 조직 기능이 아니라 RP가 참조하는 identity member list다. Exact Super Admin은 whitelist 제한의 예외지만 AAL·Fresh TOTP·계정 정상화 요구의 예외는 아니다. +- Tenant 단독 제한과 whitelist·AAL2 강제·Fresh TOTP의 지원 조건은 [로그인 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow#로그인-보안-재설계-정책-1978)과 위 유형별 조합표를 따릅니다. +- whitelist 최초 활성화의 비소급과 이후 명시적 제거의 종료 범위는 위 로그인 정책을 따릅니다. 명단 추가는 기존 사용자를 로그아웃시키지 않습니다. 제거 후에는 `로그아웃 전달 결과`에서 RP의 처리 응답을 확인합니다. +- RP에는 Factor 종류나 내부 evidence를 claim으로 전달하지 않는다. 인증 성공 또는 표준 오류 결과만 소비한다. + +유형을 변경해야 하면 기존 Client를 다른 유형으로 변환하지 않고 새 `client_id`로 등록한다. + +### whitelist·AAL2 강제·Fresh TOTP 활성화 전에 준비할 사항 + +1. RP에서 Back-Channel Logout을 실제 세션 종료에 연결합니다. 서명 검증·중복 처리·로그인 완료 경합은 [RP Session and Logout](rp-session-and-logout.md#whitelist-제거에-따른-직접-로그아웃)을 따릅니다. +2. 기본정보에서 유효한 HTTPS Back-Channel Logout URI를 등록합니다. whitelist를 사용할 때는 `backchannel_logout_session_required=false`로 설정합니다. 지원 체크박스만 선택해도 URI가 등록되는 것은 아닙니다. +3. whitelist만 사용하면 중앙 token 검증은 필요에 따라 선택합니다. AAL2 강제·Fresh TOTP도 사용하면 중앙 검증을 구현하고 기존 Client 인증 방식·Headless JWKS 조건을 충족해야 합니다. + +Tenant 단독 제한은 위 준비 절차 없이 [Tenant 선택과 정책 저장](#tenant-선택과-정책-저장)으로 진행합니다. AAL2 권장도 whitelist·AAL2 강제·Fresh TOTP를 함께 켜지 않으면 위 조건을 요구하지 않습니다. 현재 검사는 [RP 정책 유효성 검사](../../backend/internal/domain/rp_login_access.go)와 [등록 capability 검사](../../backend/internal/handler/dev_handler.go)를 기준으로 합니다. + +### Headless introspection 요청 구현 + +1. RP Backend에서 등록된 HTTPS JWKS의 공개키와 짝이 되는 private key로 assertion을 서명합니다. 실제 Client 등록 인증 방식과 Headless 설정 모두 `private_key_jwt`여야 합니다. +2. `iss`와 `sub`에는 `client_id`, `aud`에는 Baron의 `HYDRA_PUBLIC_URL`에 `/oauth2/introspect`를 붙인 전체 공개 URL을 넣습니다. 로그인 endpoint URL·상대경로·여러 audience를 넣지 않습니다. +3. `iat`는 현재 Unix 초, `exp`는 그보다 뒤이며 최대 300초 이내, `jti`는 매 요청마다 새로 생성한 고유값으로 지정합니다. `nbf`를 넣으면 현재보다 미래가 아니어야 합니다. 일반적으로 짧은 수명을 사용하고 RP 서버의 시간을 동기화해 주세요. +4. 다음 form을 POST합니다. Basic Authorization이나 `client_secret`을 함께 보내지 않습니다. 예시의 token·JWT는 RP Backend 내부에서만 취급합니다. + +```text +POST https:///oauth2/introspect +Content-Type: application/x-www-form-urlencoded + +token= +&client_id= +&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer +&client_assertion= +``` + +5. `200`에서도 `active`가 `false`면 해당 local authorization을 중단합니다. `401 invalid_client`이면 등록 정보·서명·audience·시간·assertion 재사용 여부를 확인합니다. `503`이면 처리 결과를 허용으로 해석하지 않습니다. +6. timeout·503 이후 재시도도 **새 `jti`로 새 assertion을 서명**합니다. 인증에 성공한 assertion은 token 조회나 중앙 정책 검사에서 후속 오류가 발생해도 이미 소비되었을 수 있습니다. 다른 token 조회에도 같은 assertion을 재사용하지 않습니다. 같은 Client의 다른 요청에 같은 `jti`를 사용하지 마세요. + +이 일회용 계약은 신규 introspection에만 적용됩니다. 기존 Headless 로그인·consent·poll은 기존 endpoint별 audience·challenge·session 결속 계약을 유지하며 일괄적으로 새 `jti`를 강제하지 않습니다. 현재 원격 Wiki의 기존 계약을 강화할지는 [#2164](https://gitea.hmac.kr/baron/baron-sso/issues/2164)에서 별도 검토합니다. 구현·검증 및 배포 상태는 [#2163](https://gitea.hmac.kr/baron/baron-sso/issues/2163)을 참고해 주세요. + +### Tenant 선택과 정책 저장 + +1. 대상 RP의 `로그인 보안` 탭에서 `허용 Tenant 추가`를 누릅니다. 선택한 Tenant는 이름 라벨로 표시됩니다. +2. AdminFront의 상위 Tenant 선택과 같은 트리에서 이름·슬러그로 검색하거나 펼치기 버튼으로 탐색합니다. `super_admin`에게는 활성 공개 후보의 root들이 접힌 상태로 나타나고, 검색하면 일치하는 항목의 경로가 펼쳐집니다. +3. 일반 계정은 소속·관리 권한이 있는 활성 공개 후보를 선택합니다. 한맥가족 소속 계정에는 `hanmac-family` root와 직접 자식(1depth) 중 활성 공개 Tenant도 기본 후보로 표시됩니다. 2depth 이하가 이 규칙만으로 추가되지는 않으며 기존 소속·관리 후보는 유지됩니다. 비공개 가족 하위 소속도 가족 후보를 조회할 수 있지만 비공개 Tenant 자체는 선택 후보로 나오지 않습니다. +4. 필요한 Tenant를 선택하고 여러 개면 다시 `허용 Tenant 추가`를 엽니다. 선택한 항목을 빼려면 해당 라벨의 × 버튼을 누릅니다. 후보에 더 이상 없는 기존 설정은 자동 삭제되지 않으므로 이름 대신 기존 선택 표시가 나오면 ID와 현재 상태를 확인해 주세요. +5. `Tenant 제한 저장`을 누릅니다. 성공 토스트를 확인하고 새로고침해 선택값이 유지되는지 확인합니다. whitelist·AAL2 강제·Fresh TOTP도 필요하면 위의 해당 옵션 준비 절차를 별도로 진행합니다. + +기본 후보 표시는 자동 선택·저장이 아닙니다. Tenant 선택은 RP 관리 권한이나 로그인 자격을 새로 부여하지 않습니다. 로그인 허가 판정에서는 비공개 소속만으로 상위 공개 회사 소속을 충족하지 않습니다. Tenant 제한을 비워도 기존 metadata 제한 등 다른 접근 조건이 함께 해제되지는 않습니다. 후보 API 계약은 [OpenAPI](../../backend/docs/openapi.yaml)의 `GET /api/v1/dev/clients/{id}/login-access/tenants`, 로그인 정책 정본은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow)를 참고해 주세요. + +### whitelist와 AAL 설정 + +1. whitelist를 사용하려면 먼저 허용 Tenant를 선택하고 정책을 저장합니다. +2. **화이트리스트** 섹션의 스위치를 켜면 검색·사용자 테이블이 펼쳐집니다. 끄면 제목과 스위치만 남고 명단은 삭제되지 않습니다. 테이블 위 검색란은 등록된 명단을 입력 즉시 필터링합니다. **사용자 추가** 모달에서 기존 조직도 사용자 다중선택기로 대상을 선택·확인하고 **선택한 사용자 추가**를 누릅니다. 중복·기등록 사용자는 제외하며 일부 추가 실패 시 실패 항목만 남겨 재시도할 수 있습니다. 비공개 Tenant 가져오기는 같은 모달의 **비공개 테넌트에서 가져오기**에서 변경한 정책을 저장한 뒤 사용합니다. 사용자 추가·제거는 각각 즉시 요청되며 `정책 저장`과 별개입니다. 대상은 활성 계정이며 RP의 실제 허용 소속 조건도 충족해야 합니다. +3. 명단을 확인한 뒤 `정책 저장`을 눌러 화이트리스트 사용 여부를 적용합니다. 명단 추가만으로 whitelist가 활성화되지는 않습니다. 개별 추가·제거 결과는 갱신된 명단을 확인하고, 반응이 불명확하면 새로고침해 확인합니다. +4. 추가 인증이 필요하면 `RP AAL 정책 모드`와 `RP 요구 AAL`을 함께 설정합니다. AAL2 강제는 모드 `AAL2 강제`와 수준 `AAL2`를 선택합니다. `AAL2 권장`은 강제와 다르며, `사용 안 함`도 중앙 Tenant·계정의 다른 인증 요구를 없애지는 않습니다. +5. 민감 기능에 최근 TOTP 확인이 필요하면 `민감 기능에 Fresh TOTP 요구`를 설정합니다. 일반 AAL2 충족만으로 Fresh TOTP 요구까지 충족했다고 판단하지 마세요. + +사용자를 제거한 뒤에는 `로그아웃 전달 결과`에서 대상 사용자의 UUID, 상태와 시도 횟수를 확인합니다. `전달 대기`와 `실패`는 RP 로그아웃 완료를 뜻하지 않습니다. 실패 원인을 해결하면 자동 재시도를 기다리거나 `다시 전송`을 누릅니다. 미완료 작업을 우선하여 최대 100건을 표시합니다. `전달 완료`는 RP가 성공을 응답했다는 뜻이며, 실제 세션 종료는 RP 연동 테스트로 확인해야 합니다. + +명단에는 UUID와 함께 조회 권한이 있는 사용자의 대표조직·이름·직급·이메일이 표시됩니다. RP 멤버 관리 권한만으로 개인정보 전체를 조회할 수는 없으며, 대표조직과 직급은 해당 대표 Tenant의 조회 범위 안에서 제공됩니다. 대표조직은 가져온 비공개 Tenant나 소속 Root를 뜻하지 않습니다. `정보 없음`은 권한 제한·미등록·조회 실패를 포함할 수 있으며 저장 실패를 뜻하지 않습니다. 이 경우에도 UUID로 멤버를 확인하고 제거할 수 있습니다. 구현·반영 상태는 [#2165](https://gitea.hmac.kr/baron/baron-sso/issues/2165)를 참고해 주세요. + +### 저장 결과와 오류 확인 + +| 화면 결과 | 다음 작업 | +| --- | --- | +| 저장 중 | 완료될 때까지 기다립니다. 중복 저장하지 않습니다. | +| 저장 성공 | 성공 안내 후 새로고침하여 Tenant 제한·화이트리스트·AAL 설정이 유지되는지 확인합니다. | +| HTTP 401·403 | 세션과 해당 RP의 조회·정책 변경·멤버 관리 권한을 확인합니다. 이 권한들은 서로 다릅니다. | +| HTTP 400·422 | 활성 공개 Tenant와 whitelist의 Tenant 선행조건을 확인합니다. whitelist에는 HTTPS logout URI와 subject 전체 종료 지원이 필요합니다. AAL2 강제·Fresh TOTP를 함께 사용하면 중앙 검증 지원과 Client 인증 방식도 확인합니다. | +| HTTP 404·409 | 대상 RP와 현재 저장값을 확인합니다. 다른 변경이 있으면 최신 설정을 확인한 뒤 필요한 변경만 다시 저장합니다. | +| 통신 오류·HTTP 5xx·결과 미확인 | `저장된 정책 다시 조회`로 서버 값을 확인합니다. 실패 응답이나 무응답만으로 미저장이라고 단정하지 않습니다. | + +`저장된 정책 다시 조회`는 편집 중인 초안을 유지하고 조회 시점의 Tenant 제한·화이트리스트 저장값을 별도로 표시합니다. 초안과 저장값을 비교한 뒤 재시도 여부를 결정하세요. AAL 등 다른 항목도 편집을 마친 후 새로고침하여 확인합니다. 저장 성공 후 최신 조회만 실패한 경우에는 저장 실패와 구분해서 안내합니다. + +### 비공개 Tenant에서 whitelist 가져오기 + +다음은 로컬 구현 기준의 작업 절차입니다. 운영 반영 여부는 [#2153](https://gitea.hmac.kr/baron/baron-sso/issues/2153)에서 확인합니다. 로그인 정책 정본은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow), 가져오기 API의 권한·후보·오류 계약은 [OpenAPI](../../backend/docs/openapi.yaml)의 `POST /api/v1/dev/clients/{id}/login-members/import`를 따릅니다. + +1. `허용 Tenant 추가`에서 트리를 검색하여 활성 공개 Tenant를 선택하고 먼저 `정책 저장`을 누릅니다. 기존 private 조건이 남아 있으면 실제 로그인 대상자의 공개 소속과 접근 범위를 확인해 교체합니다. 적절한 공개 후보가 없으면 제한을 임의 해제하지 말고 담당 관리자에게 소속과 설정 권한을 확인해 주세요. 로그인 허가 판정에서는 private 소속만으로 상위 회사 소속을 충족하지 않습니다. +2. `비공개 Tenant에서 가져오기`에 권한이 있는 원본 Tenant UUID를 입력하고 `미리보기`를 누릅니다. 일반 관리자는 RP의 `manage_login_members`와 원본 Tenant의 `view`·`view_users`가 모두 필요합니다. 기존 권한 상속과 정확한 `super_admin` 예외가 적용됩니다. +3. 신규 후보의 identity ID와 기존 사용자 수를 확인하고 인원수가 표시된 `N명 추가` 버튼으로 적용합니다. 후보는 원본의 직접 구성원 중 활성 상태이며 RP의 비공개가 아닌 실제 허용 소속을 충족하는 사용자입니다. 후보가 없으면 추가 버튼은 비활성화됩니다. +4. 반영된 명단을 확인합니다. 가져오기는 기존 명단을 유지하며 원본의 이후 변경을 자동 동기화하지 않습니다. 필요하면 whitelist를 별도로 활성화하고 로그인 보안 설정을 저장합니다. + +미리보기와 적용 모두 권한을 다시 검사합니다. `403`이면 RP·원본 권한을 확인하고, `409`이면 변경된 소속·정책을 확인한 뒤 미리보기부터 다시 수행합니다. 가져오기 실패를 로그인 허용으로 해석하지 않습니다. 실제 서비스 검증 방법은 [Contributor Guide](contributor.md#rp-whitelist-가져오기-실제-연동-e2e)를 참고합니다. + +## Authorization Code 흐름 + +- RP가 `state`, `nonce`, PKCE verifier/challenge를 생성하고 callback까지 검증한다. +- Baron은 login/consent challenge를 처리하지만 RP local session을 대신 소유하지 않는다. +- redirect URI는 등록 값과 exact match여야 한다. +- callback의 error도 state를 검증한 뒤 처리한다. +- access/ID token claim을 권한 원장으로 사용하지 않고 필요한 command는 Backend authorization을 다시 거친다. + +## Consent 오류 처리 + +Consent API를 직접 사용하는 RP는 [OpenAPI의 ConsentFailureResponse](../../backend/docs/openapi.yaml)의 `code`로 실패 원인을 구분합니다. `status`는 실제 HTTP 상태와 같습니다. +`message`는 선택적인 영문 설명이며 번역하지 않습니다. `next_action`은 다음 행동을 나타내며, `request_id`는 문의와 서버 로그를 연결합니다. + +RP는 오류 상세에 HTTP 상태와 `code`를 표시합니다. `message`가 있으면 영문 그대로 표시하며, 문자열을 해석해서 오류 종류를 결정하지 않습니다. +`pkce_restart_required`만으로 PKCE verifier 불일치를 단정하지 않습니다. 같은 challenge로 자동 재시도하지 않습니다. + +`restart_login`이면 사용자 동작으로 새 authorization 요청을 시작한다. `contact_support`이면 인증 조건을 확인·해결한 뒤 새 로그인을 시작한다. 알 수 없는 행동에는 문의 안내를 제공한다. 새 흐름에서는 기존 state, nonce, PKCE verifier/challenge를 재사용하지 않는다. + +브라우저 리다이렉트형 RP는 이 API JSON을 callback으로 자동 수신하지 않는다. 해당 오류는 UserFront가 원인·다음 행동·문의용 요청 ID로 표시한다. RP callback은 기존 OIDC 오류와 state 검증 계약을 유지하며 임의 redirect URL로 상세 오류를 전달하지 않는다. + +## Installed RP 시간 오류 처리 + +오류 계약과 영문 상세 정책은 [Error Handling Policy](https://gitea.hmac.kr/baron/baron-sso/wiki/Error-Handling-Policy)에서 관리합니다. +구현 전환은 [#2186](https://gitea.hmac.kr/baron/baron-sso/issues/2186)에서 추적합니다. + +설치형 RP의 authorization completion은 시간 오류에서 HTTP 400과 `status=400`을 반환합니다. `code`는 `device_proof_expired`처럼 서버가 확인한 실패를 식별합니다. +시간 진단에는 `stage=installed_device_proof`, `next_action`, `retryable=false`, `request_id`도 포함됩니다. 선택적인 `message`는 영문 그대로 표시합니다. + +시간 진단은 만료, 아직 사용할 수 없는 시각, 허용 범위를 벗어난 미래·과거 발급 시각, 필수 시간 값·유효기간 규칙 위반을 구분합니다. 예를 들어 `device_proof_issued_at_future`는 발급 시각이 서버의 허용 미래 범위를 벗어났다는 뜻이며, 실제 기기 시계가 틀렸다고 확정하는 진단은 아닙니다. 정확한 원인 코드와 응답 필드는 [OpenAPI의 InstalledDeviceProofTimeFailureResponse](../../backend/docs/openapi.yaml)를 따릅니다. 일반 timeout이나 PKCE verifier 불일치로 치환하지 않습니다. + +`restart_login`이면 기기·서버의 시간 동기화와 RP의 proof 생성 설정을 확인한 뒤 사용자 동작으로 새 로그인을 시작합니다. `contact_support`이면 요청 ID와 함께 관리자에게 문의합니다. 알 수 없거나 계약과 맞지 않는 진단은 일반 입력 오류로 처리하며, 같은 proof와 authorization code를 자동 재전송하지 않습니다. 상세 복구 절차는 [설치형 RP 가이드의 Device Proof 시간 오류 처리](rp-installed.md#device-proof-시간-오류-처리)를 참고합니다. + +## Headless 흐름 + +- 자체 동의 화면을 구현하는 Consent API 절차와 Baron 제공 JS UI의 탑재 방법은 [Headless RP Consent 가이드](rp-headless-consent.md)를 따른다. 독립 JS UI와 RP별 consent 진입 계약의 검증·배포 상태는 [#2104](https://gitea.hmac.kr/baron/baron-sso/issues/2104)에서 추적한다. +- Headless RP는 Backend에서 private key를 보관하고 `private_key_jwt`를 사용한다. +- JWKS는 HTTPS로 제공하고 현재 key와 rotation overlap을 관리한다. +- phone/link login의 `pendingRef`와 polling 상태를 정상 상태로 처리한다. +- redirect가 아직 없다는 이유만으로 pending을 실패로 바꾸지 않는다. +- consent와 Tenant 제한은 일반 OIDC 흐름과 같은 원장 검사를 사용한다. + +## 관계와 운영 권한 + +- RP owner Tenant와 RP 접근 대상은 다른 relation이다. +- 조회, 설정 편집, consent 관리, audit 조회, installed device 운영 권한을 분리한다. +- 화면의 capability projection은 action 표시용이며 Backend가 mutation을 재검사한다. +- 직접 부여와 Tenant 상속의 결과를 raw tuple 하나로 추측하지 않는다. + +## 검증 + +- authorization 시작과 callback state 검증 +- invalid redirect/JWKS/client assertion 거부 +- consent accept/reject와 선택 scope +- logout과 token revoke +- unauthorized Tenant/RP actor 거부 +- secret, code, token이 log에 남지 않는지 확인 +- 중앙 introspection과 refresh에서 policy revision, membership, credential, Tenant 요구 변경이 반영되는지 확인 +- whitelist 단독 사용에서 중앙 검증 없이 정책을 저장하고 명단 밖 신규 로그인을 거부하는지 확인 +- 명단 제거 후 직접 logout 수신·실패 재시도·기존 RP 세션 종료와 다른 사용자 비영향 확인 + +설치형 RP는 [Installed RP Guide](rp-installed.md), session 종료는 [RP Session and Logout](rp-session-and-logout.md), 정확한 API는 Backend OpenAPI를 따른다. + +## 설치형 앱의 세션 복구와 다중 프로세스 + +DevFront의 **RP 상세 → 설정 → 애플리케이션 프로필**에서 **설치형 앱**을 선택한 뒤, Device quota 입력 칸과 같은 줄의 **RP 복구 발급** 스위치를 켜고 앱이 completion에 `sessionRefreshSupported=true`를 보내면 Baron 복구 자격증명을 발급합니다. RP별 기본값은 차단이며, 허용 여부를 운영자가 선택합니다. 서버 전체 기능의 기본값은 허용입니다. `INSTALLED_SESSION_REFRESH_ENABLED=false`는 긴급 전체 차단에만 사용하며, 기존 환경 파일에 명시된 `false`는 기본값 변경 후에도 차단으로 적용됩니다. Hydra access/refresh token은 계속 서버 Vault에만 보관합니다. 앱은 OAuth token endpoint 대신 [OpenAPI](../../backend/docs/openapi.yaml)의 installed sessions refresh/revoke API를 사용합니다. + +기본 설정에서 짧은 세션은 15분이며 유효한 operation으로 연장됩니다. 복구 자격증명은 마지막 성공한 복구 후 7일 미사용 만료와 최초 PKCE 인증 후 30일 절대 만료를 함께 적용합니다. 짧은 세션의 활동만으로 복구 자격증명의 미사용 만료를 연장하지 않습니다. 복구 성공은 절대 만료를 연장하지 않습니다. 프로그램을 종료하고 다음 날 실행해도 두 기한 안이고 중앙에서 폐기되지 않았다면 로그인 화면 없이 복구할 수 있습니다. 실제 기한은 응답의 만료 필드로 판단하세요. 기한 초과나 명시적 폐기 뒤에는 PKCE 로그인이 필요합니다. + +여러 프로세스는 같은 issuer·RP·OS 사용자·enrollment 저장소와 프로세스 간 잠금을 사용합니다. 잠금을 획득한 후 저장소를 다시 읽고, 이미 갱신된 세션이면 그대로 사용합니다. 갱신 담당 프로세스만 요청하고 새 자격증명·세션·generation을 OS 보안 저장소에 원자적으로 저장합니다. 서로 다른 사용자 계정의 자격증명은 공유하지 않습니다. 기기 private key와 복구 자격증명을 함께 보호해야 합니다. + +1. 네트워크 호출 전에 현재 자격증명과 logical Idempotency-Key, 시작 시각을 영속 저장합니다. +2. 각 시도에서 새 `jti`로 proof를 서명합니다. 재시도에서도 자격증명과 Idempotency-Key는 그대로 둡니다. +3. 응답 유실 시 60초 안에 같은 요청을 재시도합니다. 이미 완료된 요청은 같은 결과를 반환합니다. 새 자격증명에는 새 Idempotency-Key를 사용합니다. +4. HTTP 409 `refresh_in_flight`는 잠시 후 같은 요청으로 재시도합니다. 503을 곧바로 로그인 실패로 처리하지 않습니다. 결과가 불확실하면 서버가 해당 인증을 차단할 수 있으며 이후 401에서는 새 로그인이 필요합니다. +5. 정상 복구는 같은 인증에 속한 다른 프로세스의 아직 유효한 세션을 폐기하지 않습니다. 이전 자격증명을 다른 요청에 재사용하면 해당 인증 전체가 폐기됩니다. + +로그아웃은 revoke API가 성공하기 전에 로컬 폐기 의도를 저장하고 세션 사용부터 중단합니다. 오프라인이면 폐기 요청을 다음 연결 때 재시도하며 이 상태에서 자동 복구하지 않습니다. 로컬 파일 삭제만으로 서버 인증을 폐기했다고 판단하지 마세요. + +RP별 발급·갱신 허용은 위 설정 화면의 **RP 복구 발급** 스위치에서 변경합니다. 스위치 변경은 즉시 저장되며 **Device quota 저장**과 별개입니다. **복구 키** 메뉴에서는 사용자 ID로 조회하여 특정 인증을 폐기할 수 있습니다. 복구 발급 스위치를 끄면 확인창이 표시되며, **복구 발급 끄기 확인**을 선택하면 해당 RP의 기존 복구 키도 무효화합니다. 다시 켜도 이전 키는 되살아나지 않으므로 새 PKCE 로그인에서 새 키를 발급받아야 합니다. 아직 유효한 짧은 세션과 다른 RP의 설정·복구 키는 유지됩니다. 다른 관리자가 먼저 설정을 변경했다면 화면을 갱신한 뒤 다시 저장하세요. 조회에는 device inventory 권한, 설정 변경·폐기에는 device policy 관리 권한이 필요하며 super 관리자는 직권 폐기할 수 있습니다. 화면에는 자격증명 원문이 없습니다. 복구 키 목록의 사용자·디바이스를 확인한 뒤 행의 **폐기**를 선택하고 확인합니다. 이 개별 폐기는 해당 인증의 모든 세션에 적용되며 다른 사용자의 인증을 보존합니다. 외부 세션 종료와의 연결은 [Session and Logout](rp-session-and-logout.md)을 참고하세요. + +운영 시 `INSTALLED_SESSION_REFRESH_IDLE_TTL`(기본 `168h`), `INSTALLED_SESSION_REFRESH_ABSOLUTE_TTL`(`720h`)로 기한을 설정합니다. 설정을 줄이면 기존 인증에도 더 짧은 기한을 적용하며 이후 늘려도 이전 기한을 되살리지 않습니다. 기능을 껐다 켜도 기존 복구 자격증명은 다시 유효해지지 않습니다. 이미 유효한 짧은 세션은 원래 기한까지 사용할 수 있습니다. 데이터베이스 복원 시 트래픽을 열기 전에 `INSTALLED_SESSION_REFRESH_EPOCH`를 이전에 사용하지 않은 값으로 바꾸세요. 모든 서버에 동일한 설정을 적용하고 순차 배포 중 서로 다른 정책이 경쟁하지 않게 하세요. + +갱신과 폐기 endpoint는 Backend 인스턴스별 IP당 합산 분당 60회 제한을 적용합니다. 다중 인스턴스 전체 제한은 gateway에서 별도로 구성하세요. PostgreSQL 연결 풀은 refresh advisory lock과 내부 transaction이 동시에 사용할 연결 여유가 필요합니다. 자격증명, proof, 응답 본문을 로그에 남기지 마세요. 폐기 outbox 전달 상태와 409·503 비율을 운영 모니터링에 포함하세요. + +### 갱신에 사용할 값과 최초 응답 + +갱신 요청의 `sessionRefreshCredential`에는 **직전 성공한 completion 또는 refresh 응답의 `sessionRefreshCredential` 문자열 원문**을 넣습니다. 앱이 임의로 생성하거나 hash로 바꿔 보내지 않습니다. 다음 값의 용도를 구분하세요. + +| 값 | RP에서 사용하는 곳 | +|---|---| +| `sessionRefreshCredential` | 복구·폐기 요청 JSON 본문. 성공할 때마다 최신 값으로 교체 | +| `deviceSessionHandle` | 일반 operation의 `X-Baron-Device-Session` 헤더 | +| 기기 private key | `X-Baron-Device-Proof` JWT 서명. 키 원문은 전송하지 않음 | +| `Idempotency-Key` | RP가 새 갱신마다 생성하는 고유 요청 식별자. 동일 요청 재시도에는 재사용 | +| `refreshGeneration` | 응답의 자격증명 세대 번호. 인증 수단으로 전송하지 않음 | + +최초 로그인은 `POST /api/v1/rp/installed/authorizations/complete`의 기존 PKCE·기기 등록 필드에 `sessionRefreshSupported: true`를 추가합니다. 해당 RP에서 복구 발급을 허용했고 서버 전체가 차단되지 않았다면 HTTP 200 JSON 본문에 복구 필드가 추가됩니다. 아래 문자열과 시각은 예시이며 실제 값으로 대체해야 합니다. + +```json +{ + "deviceId": "7bd0c106-608e-4d57-8d37-c95b2bc94613", + "deviceSessionHandle": "", + "sessionExpiresAt": "2026-09-21T09:15:00Z", + "sessionRefreshCredential": "", + "refreshIdleExpiresAt": "2026-09-28T09:00:00Z", + "refreshAbsoluteExpiresAt": "2026-10-21T09:00:00Z", + "refreshGeneration": 1 +} +``` + +Hydra `access_token`·`refresh_token`은 응답에 없습니다. RP에서 복구 발급을 차단했거나 서버 전체가 차단된 경우, 또는 앱이 opt-in하지 않으면 복구 관련 네 필드가 생략됩니다. RP는 필드 존재 여부를 확인해야 하며, 복구 자격증명이 없는 상태에서 짧은 세션까지 만료되면 PKCE 로그인을 시작합니다. + +### 갱신 요청 만들기 + +앱 재시작이나 짧은 세션 만료 시, 앞 절의 공유 잠금을 획득하고 저장소를 다시 읽습니다. 복구 자격증명의 미사용·절대 만료가 지나지 않았으면 다음 순서로 요청합니다. + +1. 저장소에 pending 요청이 있으면 다른 프로세스가 남긴 요청을 이어받습니다. 새 요청 키를 만들지 않습니다. pending 요청이 없는 새 논리 갱신에만 `Idempotency-Key`를 생성합니다. UUID 같은 고유 문자열을 사용하며 최대 128자입니다. 현재 자격증명·요청 키·시작 시각을 네트워크 요청 전에 영속 저장합니다. +2. 해당 기기에 등록된 private key로 proof JWT를 서명합니다. 아래 표의 header와 claim을 넣습니다. +3. 최신 `sessionRefreshCredential` 원문을 JSON 본문에 담아 Baron 공개 Backend URL로 POST합니다. 이 호출은 짧은 세션이 만료된 뒤에도 가능하며 `X-Baron-Device-Session` 헤더를 요구하지 않습니다. + +| Proof 위치 | 필드 | 넣을 값 | +|---|---|---| +| JWT header | `typ` | `baron-session-refresh+jwt` | +| JWT header | `alg`, `kid` | 등록한 기기 JWK와 일치하는 알고리즘과 public JWK thumbprint | +| JWT claim | `iss`, `sub` | 둘 다 `device:{deviceId}` | +| JWT claim | `aud` | 서버의 `BACKEND_PUBLIC_URL` 뒤에 `/api/v1/rp/installed/sessions/refresh`를 붙인 정확한 URL | +| JWT claim | `htm`, `client_id` | `POST`, 요청 본문과 같은 RP client ID | +| JWT claim | `credential_hash` | `base64url_no_padding(SHA256(UTF8(sessionRefreshCredential)))` | +| JWT claim | `idempotency_key_hash` | `base64url_no_padding(SHA256(UTF8(Idempotency-Key)))` | +| JWT claim | `iat`, `exp` | Unix 초 단위 발급·만료 시각. TTL은 최대 5분이며 짧게 설정 | +| JWT claim | `jti` | 매 HTTP 시도마다 새로 생성한 고유 값 | + +기존 기기 등록 때 사용한 `kid`와 서명 알고리즘을 그대로 사용합니다. 서버는 public JWK의 SHA-256 thumbprint를 padding 없는 base64url로 인코딩한 `kid`를 검사합니다. 허용 알고리즘은 `RS256`, `PS256`, `ES256`, `ES384`, `ES512`, `EdDSA`이며 키 종류·크기·곡선도 일치해야 합니다. 구체적인 키 검증은 [기기 public JWK 검증 코드](../../backend/internal/domain/rp_device.go)를 참고하세요. + +hash는 문자열 원문의 UTF-8 바이트로 계산합니다. JSON 따옴표나 줄바꿈을 포함하지 않습니다. proof의 `iat`가 서버 시각보다 30초 넘게 미래이면 거절됩니다. 예시의 호스트는 배포된 공개 URL로 바꾸며, reverse proxy 내부 주소를 `aud`에 사용하지 않습니다. 정확한 계약은 [OpenAPI](../../backend/docs/openapi.yaml)의 installed sessions endpoint를 따릅니다. + +```http +POST /api/v1/rp/installed/sessions/refresh HTTP/1.1 +Host: sso.example.com +Content-Type: application/json +X-Baron-Device-Proof: +Idempotency-Key: 484b2d71-3198-4ef5-a088-626081167c9c + +{ + "clientId": "my-installed-rp", + "deviceId": "7bd0c106-608e-4d57-8d37-c95b2bc94613", + "sessionRefreshCredential": "<직전 성공 응답에서 받은 원문>" +} +``` + +### 응답 저장과 실패 처리 + +성공하면 `Cache-Control: no-store`와 함께 HTTP 200 JSON을 반환합니다. 형태는 최초 응답과 같으며 새 `deviceSessionHandle`, 새 `sessionRefreshCredential`, 새 만료 시각과 증가한 `refreshGeneration`을 받습니다. 예를 들어 최초 세대가 1이면 다음 성공 갱신은 2입니다. `refreshAbsoluteExpiresAt`는 최초 인증의 기한을 넘어 늘어나지 않습니다. + +공유 잠금을 유지한 채 응답 전체의 세션·자격증명·기한·세대를 한 묶음으로 원자적으로 저장하고, pending 요청을 지운 뒤 잠금을 해제합니다. 다음 프로세스는 저장소의 새 값을 읽습니다. 응답을 받지 못했다면 pending 요청을 유지하고 60초 안에 **같은 자격증명과 같은 Idempotency-Key, 새 `jti`로 서명한 proof**로 재시도합니다. 서버가 이미 갱신을 완료했다면 같은 결과를 돌려주므로 세대가 다시 증가하지 않습니다. 저장 완료 후 다음 갱신에는 새 자격증명과 새 요청 키를 사용합니다. + +| HTTP / 응답 `code` | RP 처리 | +|---|---| +| `400 invalid_device_proof` | 서명·audience·hash·시각·jti를 확인. 같은 proof를 재전송하지 않음 | +| `401 reauthentication_required` | 해당 로컬 인증 사용을 중단하고 PKCE 로그인 시작 | +| `401 session_refresh_idle_expired` | 미사용 기한이 지난 복구 자격 사용을 중단하고 PKCE 로그인 시작 | +| `401 session_refresh_absolute_expired` | 절대 기한이 지난 복구 자격 사용을 중단하고 PKCE 로그인 시작 | +| `409 refresh_in_flight` | `Retry-After`를 지키고 같은 논리 요청으로 재시도 | +| `409 idempotency_conflict` | 같은 요청 키에 다른 자격증명을 붙였는지 확인. 저장된 pending 요청과 대조 | +| `429` | 요청을 늦추고 응답 유실 재시도 기한도 확인 | +| `503 temporarily_unavailable` / `operation_indeterminate` | pending 요청 보존. 같은 요청으로 재시도하며, 이후 401이면 재로그인 | + +일반 operation이 `401 session_expired`와 `next_action=refresh_session`을 반환하면 짧은 세션의 시간 만료입니다. 저장한 `sessionRefreshCredential`이 있고, 최초 발급 또는 마지막 갱신 응답의 `refreshIdleExpiresAt`와 `refreshAbsoluteExpiresAt`가 모두 지나지 않았다면 `POST /api/v1/rp/installed/sessions/refresh`를 먼저 호출하세요. 최종 유효성은 서버가 판단합니다. 자격증명이 없거나 두 기한 중 하나라도 지났거나 갱신이 위 표의 HTTP 401 코드로 거절되면 PKCE 로그인을 시작하세요. `503`은 위 표의 재시도 절차를 따르며, 만료 안내 자체가 갱신 성공을 보장하지는 않습니다. + +갱신 API는 확인된 만료 원인을 최상위 `code`로 반환합니다. 미사용 기한 만료는 `session_refresh_idle_expired`, 절대 기한 만료는 `session_refresh_absolute_expired`이며 둘 다 HTTP 401과 `next_action=restart_login`을 반환합니다. 두 기한이 모두 지났으면 절대 기한을 표시합니다. 명시적 폐기나 알 수 없는 자격은 기존 `reauthentication_required`를 유지하며 시간 만료로 표시하지 않습니다. `message`가 있으면 영문 그대로 표시하고 행동 판정에는 `code`와 선언된 `next_action`을 사용하세요. + +#2268에서 기존 `reauthentication_required` 아래의 `details.code`·`details.message`를 최상위 `code`·`message`로 옮겼습니다. 기존 응답을 처리하던 RP는 위의 세 HTTP 401 코드를 모두 재로그인 대상으로 처리해야 합니다. 현재 응답에는 중첩 `details`가 없습니다. [오류 코드 안내](api-errors.md)에서 각 원인과 조치를 확인하세요. + +일반 OAuth RP의 token endpoint는 별도 프로토콜 계약을 사용합니다. 확인된 inactive refresh token 또는 다른 client에 속한 token은 `401 invalid_grant`이며 새 로그인이 필요합니다. introspection 통신 장애나 잘못된 공급자 응답은 `503 temporarily_unavailable`입니다. `invalid_grant`만으로 만료와 폐기를 구분할 수 없으며 Baron의 `details` 필드는 이 응답에 추가하지 않습니다. + +서버의 응답 재생 창은 성공 결과 생성 시각부터 60초입니다. RP는 서버 완료 시각을 알 수 없으므로 SDK처럼 **영속 저장한 최초 요청 시작 시각부터 60초**를 보수적인 재시도 한도로 사용합니다. 프로세스를 재시작하거나 담당 프로세스가 바뀌어도 이 시작 시각을 초기화하지 않습니다. 자격증명의 미사용·절대 만료도 별도로 확인합니다. + +응답 유실 후 이 60초 재시도 한도를 넘겼다면 이전 자격증명을 새 요청 키로 다시 보내지 말고 PKCE 로그인을 시작합니다. 관리자나 외부 세션 종료에 따른 폐기는 다음 operation 또는 refresh 호출에서 확인합니다. 이번 연동에는 별도의 실시간 폐기 push가 없습니다. + +앱 로그아웃은 같은 JSON 구조로 `POST /api/v1/rp/installed/sessions/revoke`를 호출합니다. 별도 폐기 요청 키를 영속 저장하고, proof의 `typ`을 `baron-session-revoke+jwt`, `aud`를 해당 revoke endpoint의 공개 URL로 바꿔 새로 서명합니다. 폐기 의도·자격증명·폐기 요청 키는 모든 프로세스가 보는 공유 보안 저장소에 기록합니다. 기기 키도 재시도에 필요하므로 유지합니다. pending 폐기가 있으면 자동 복구를 중단하고 폐기를 먼저 재시도합니다. 성공 응답은 본문 없는 HTTP 204이며, 이를 확인한 뒤 저장된 복구 자격증명과 pending 폐기 상태를 지웁니다. 401을 포함한 다른 응답을 폐기 완료로 간주하지 않습니다. 오프라인 폐기와 다중 프로세스 처리 순서는 앞 절을 따릅니다. diff --git a/docs/sso_guides/rp-session-and-logout.md b/docs/sso_guides/rp-session-and-logout.md new file mode 100644 index 0000000..d88e156 --- /dev/null +++ b/docs/sso_guides/rp-session-and-logout.md @@ -0,0 +1,103 @@ +# RP Session and Logout Guide + +## 자동 로그인 + +UserFront에서 RP를 선택했을 때 자동 로그인을 지원하려면 RP의 시작 URL이 직접 OIDC Authorization Code 흐름을 시작해야 한다. + +- RP가 `state`, `nonce`, PKCE를 생성한다. +- UserFront나 Baron Backend가 RP 대신 authorization URL의 보안 상태를 만들지 않는다. +- RP 시작 URL은 open redirect가 아니어야 한다. +- 이미 유효한 Baron session이 있으면 login UI를 생략할 수 있지만 callback 검증은 생략하지 않는다. + +## Session 소유권 + +Backend API에 전달하는 Kratos Session Token과 Hydra Access Token의 구분은 Wiki의 [Backend 보호 API의 credential 구분](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow#backend-보호-api의-credential-구분)을 참고해 주세요. + +- Kratos session: Baron 로그인 session +- Hydra authorization, consent, token chain: RP별 OAuth/OIDC protocol 상태 +- RP local session: RP가 생성하고 삭제하는 서비스 session + +한 계층의 종료를 다른 계층의 완료로 추측하지 않는다. logout은 지원 가능한 전파 방식과 사후 검증을 가진다. + +Kratos session과 Hydra authorization은 일대일 관계가 아니다. 한 Kratos session에서 여러 RP authorization이 만들어질 수 있고, 같은 RP도 여러 authorization 처리와 token chain을 가질 수 있다. consent는 authorization 과정에서 scope와 claim 제공을 승인하는 단계이며 로그인 session을 뜻하지 않는다. Hydra의 `consent_request_id`는 개별 authorization 처리 건을 식별하는 provider 기술 키다. + +## 특정 RP 연결 해제 + +`DELETE /api/v1/user/rp/linked/{clientId}`는 현재 사용자의 지정 RP 연결을 해제한다. 처리 단위는 특정 Kratos session이 아니라 `subject + client_id`에 속한 사용자 RP authorization 전체다. + +- 해당 RP의 Hydra access/refresh token chain을 모두 폐기한다. +- interactive installed RP는 Baron Token Vault의 local token family와 opaque session authorization을 차단하고 저장된 refresh token revoke 전달을 outbox에 기록한다. 외부 revoke 전달 완료는 worker 상태로 별도 확인한다. +- Back-Channel Logout URI가 있으면 subject-only logout token 전파를 시도한다. 현재 요청에는 `sid`를 넣지 않으므로 `sid`가 반드시 필요한 RP에는 전파할 수 없다. +- M2M `client_credentials`는 사용자 연결이 아니므로 이 API의 대상이 아니다. +- Kratos session 자체는 종료하지 않는다. + +HTTP 200은 Baron local authorization fence와 Hydra의 사용자·RP token chain revoke 요청이 완료되었다는 뜻이다. 외부 refresh token revoke 전달, Back-Channel Logout 전파 성공, RP local session 종료 완료를 뜻하지 않는다. + +Hydra revoke 뒤 local consent 삭제가 일시적으로 실패하면 같은 요청을 재시도해 정리를 완료할 수 있다. 이미 전체 정리가 끝난 뒤 같은 DELETE를 다시 보내면 현재는 연결 없음으로 404를 반환한다. + +`DELETE /api/v1/user/sessions/{sessionId}`는 해당 source session에 결합된 설치형 인증·복구 자격증명·짧은 세션을 함께 폐기한다. 같은 계정의 다른 source session에 결합된 설치형 인증은 보존한다. + +`DELETE /api/v1/user/sessions`는 서버가 검증한 현재 session을 제외한 대상 목록을 고정하고, 대상 source session의 설치형 인증을 차단한 뒤 Kratos session을 각각 종료한다. 같은 Idempotency-Key의 재시도는 최초 대상만 처리하므로 이후 생성한 session을 폐기하지 않는다. 설치형 Hydra token 폐기는 durable outbox로 전달한다. 이 bulk API는 일반 RP Back-Channel Logout을 전파하지 않는다. + +## Back-Channel Logout + +Back-Channel Logout을 지원하는 RP는 등록한 HTTPS endpoint에서 Baron 요청을 받는다. + +다른 RP 설정을 편집할 때 기존 로그아웃 설정을 보존하는 기준은 [메타데이터 스키마 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Metadata-Schema-Design.-.-#rp-구성)을 참고해 주세요. + +Baron은 RP가 등록한 Back-Channel Logout URI 전체에 `POST`하고, `application/x-www-form-urlencoded` body의 `logout_token=`를 전송한다. 현재 logout token은 `sub`를 포함하고 `sid`는 포함하지 않는다. RP는 `/api/v1/auth/backchannel/jwks.json`의 key로 token을 검증한다. + +1. JWT signature와 `iss`, `aud`, `iat`, `jti`를 검증한다. +2. `events`에 `http://schemas.openid.net/event/backchannel-logout`가 있고 `nonce`가 없는지 확인한다. +3. `sid` 또는 `sub`가 있는지 확인하고 local session을 찾는다. +4. 같은 `jti`의 재전송이 중복 side effect를 만들지 않게 처리한다. +5. token이나 session 원문을 로그에 남기지 않는다. +6. 이미 종료된 session의 logout도 안전한 성공으로 처리한다. + +## whitelist 제거에 따른 직접 로그아웃 + +활성 whitelist에서 사용자를 제거하면 Baron은 RP의 다음 중앙 검증 요청을 기다리지 않고 로그아웃을 전송합니다. 적용 범위와 최초 활성화의 비소급은 [로그인 보안 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow#로그인-보안-재설계-정책-1978)을 따릅니다. 이 경로는 `sub`로 사용자의 RP 세션 전체를 대상으로 하므로 `backchannel_logout_session_required=false`가 필요합니다. URI 등록만으로 실제 세션 종료가 구현되는 것은 아닙니다. + +RP 수신 endpoint는 다음 절차를 따릅니다. + +1. 서명과 `iss`, `aud`, `iat`, `exp`, `jti`, `events`를 검증합니다. 이 경로에서는 `sub`가 필수이며, 검증한 `iss`와 `sub`로 RP 사용자를 찾습니다. +2. 이미 처리한 `jti`이면 세션 종료를 반복하지 않고 200 또는 204를 반환합니다. +3. 새 사건이면 대상 사용자의 자체 세션과 관련 권한 캐시를 종료합니다. +4. 세션 종료와 처리한 `jti`를 같은 transaction에 저장합니다. 별도 권한 캐시 정리가 실패하면 처리 완료를 기록하지 않고 재시도로 정리를 마쳐야 합니다. +5. 종료를 완료한 뒤 200 또는 204를 반환합니다. 202는 완료 응답으로 처리되지 않습니다. + +Baron은 같은 전송 사건에 같은 `jti`를 사용합니다. 재시도할 때는 유효한 `iat`와 `exp`로 다시 서명합니다. 현재 전송 작업에는 자동·수동 재시도의 최대 보관 기한이 없으므로 RP의 처리한 `jti` 기록을 자동 만료시키지 않습니다. 세션을 실제로 종료한 뒤 응답이 유실되어도 다음 요청에 안전하게 성공을 응답해야 합니다. + +서명이나 claim 검증이 실패하면 400을 반환합니다. 세션 종료나 기록 저장이 실패하면 성공 응답을 반환하지 않습니다. Baron은 통신 오류와 200·204 이외의 응답을 미완료로 기록하고 간격을 늘려 재시도합니다. 설정 오류도 성공으로 처리하지 않으므로 운영자가 전달 결과의 원인을 확인하고 수정해야 합니다. + +로그아웃 뒤에 오래된 로그인 callback이 도착하는 경우도 처리해야 합니다. 한 구현 방법은 RP 전체의 영속 종료 순번을 서버의 로그인 transaction에 시작 시 기록하는 것입니다. logout 수신 시 순번을 원자적으로 증가시키고 대상 subject의 마지막 종료 순번과 세션 삭제를 함께 저장합니다. callback에서 검증한 subject의 종료 순번이 로그인 시작 순번보다 크면 해당 로그인을 거부합니다. 순번 비교와 세션 생성은 logout 처리와 같은 subject 잠금으로 직렬화합니다. 만료되었거나 이미 완료한 로그인 transaction은 재사용하지 않습니다. + +여러 RP 인스턴스는 같은 세션·중복 처리·종료 순번 원장을 사용해야 합니다. subject의 종료 순번 기록은 관련 미완료 로그인 transaction이 만료될 때까지 보관하며, `jti` 중복 처리 기록과 구분합니다. 새 로그인에는 새 state·nonce·PKCE를 사용합니다. 다만 삭제 뒤 재추가한 사용자는 새 로그인 후에도 늦게 도착한 통지로 다시 로그아웃될 수 있습니다. + +명단 저장 성공은 RP의 로그아웃 완료를 뜻하지 않습니다. DevFront의 `로그아웃 전달 결과`에서 실패 원인과 재시도를 확인합니다. 통신 장애 중에는 기존 접근이 남을 수 있으며, 허용 시간을 제한하려면 RP 자체 세션에 절대 만료를 설정합니다. 사용 중 자동 연장만 있는 세션은 이 상한을 제공하지 않습니다. 별도 API가 자체적으로 허용하는 bearer token은 해당 API의 검증·폐기 정책으로 다뤄야 합니다. + +실제 Docker 검증과 구현 기록은 [#2481](https://gitea.hmac.kr/baron/baron-sso/issues/2481)에 있습니다. 명단 추가·재추가가 추가 로그아웃을 만들지 않는지, 실패 후 재시도와 중복 수신이 안전한지, 다른 사용자의 세션이 유지되는지 확인합니다. + +## Back-Channel을 사용할 수 없는 경우 + +- token refresh가 거부되면 local authorization을 종료한다. +- 주기적 또는 중요 action 전 `/user/me` 같은 검증 endpoint로 Baron session 상태를 확인한다. +- offline 상태와 명시적 logout을 같은 오류로 처리하지 않는다. +- public client는 client secret을 포함하지 않는다. + +## UserFront와 원격 요청 + +- UserFront는 Kratos session 상태를 기준으로 logout을 반영한다. +- SMS 또는 NAVER WORKS logout link는 사용자 확인 뒤 Backend가 처리한다. +- RP 유형에 따라 token revoke와 Back-Channel 전달 순서가 달라도 local session 제거의 최종 결과는 같아야 한다. + +## 검증 항목 + +- 동일 요청 재전송 +- 만료·잘못된 서명·다른 audience 거부 +- 이미 삭제된 session +- RP endpoint timeout과 retry +- refresh revoke 뒤 local session 종료 +- 로그와 audit의 민감정보 마스킹 + +인증 흐름의 정책 정본은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow)다. diff --git a/docs/sso_guides/userfront.md b/docs/sso_guides/userfront.md new file mode 100644 index 0000000..670fda8 --- /dev/null +++ b/docs/sso_guides/userfront.md @@ -0,0 +1,67 @@ +# UserFront 사용자 가이드 + +## 로그인 + +UserFront는 Baron SSO 로그인, 연결 승인, consent, 연동 앱 탐색의 사용자 접점이다. + +1. 서비스 또는 UserFront에서 로그인을 시작한다. +2. email, phone, employee ID 등 화면에 허용된 식별자를 입력한다. +3. 다른 기기 승인이나 link가 필요한 경우 화면의 pending 상태를 유지한다. +4. 승인 후 원래 서비스 또는 UserFront로 돌아오는지 확인한다. + +`잠시 사용할 수 없음`이나 readiness 오류를 미가입·비밀번호 오류로 해석하지 않는다. 잠시 뒤 재시도하고 계속 실패하면 표시된 추적 정보와 시각을 운영자에게 전달한다. + +## Consent + +- 필수 scope는 해제할 수 없다. +- 선택 scope는 서비스에 제공할 정보 범위를 보고 선택한다. +- 동의를 거부하면 로그인 시도는 원래 RP로 오류와 함께 돌아갈 수 있다. +- 과거 동의는 활동상황 또는 연결된 서비스 화면에서 확인한다. +- 연동 해지는 해당 RP의 consent와 authorization에 영향을 주며 다음 접속에서 다시 동의를 요구할 수 있다. + +## 연결된 서비스 + +- RP card를 선택하면 등록된 서비스 시작 URL 또는 홈페이지로 이동한다. +- 자동 로그인 지원 RP는 별도 login button 없이 OIDC 흐름을 시작할 수 있다. +- UserFront가 RP의 `state`, `nonce`, PKCE를 대신 만들지 않는다. +- 예상하지 않은 도메인이나 redirect가 보이면 진행하지 않고 운영자에게 알린다. + +## QR과 다른 기기 승인 + +- QR을 표시한 기기가 로그인 대상이고 스캔 기기는 승인을 수행한다. +- QR이나 link가 만료되면 현재 화면에서 새 요청을 발급한다. +- `pending`은 실패가 아니다. 승인 완료 또는 만료까지 화면 안내를 따른다. +- 같은 요청을 여러 창에서 반복 승인하지 않는다. + +## 로그아웃 + +- UserFront 로그아웃은 Kratos session을 종료한다. +- 연결된 RP는 Back-Channel Logout, token refresh 실패, `/user/me` 재검증 등 지원 방식에 따라 local session을 정리한다. +- SMS 또는 NAVER WORKS 원격 로그아웃 link는 대상 서비스와 요청 시각을 확인한 뒤 승인한다. +- 의심스러운 요청은 승인하지 말고 UserFront에서 직접 session 상태를 확인한다. + +## 비밀번호와 복구 + +1. 로그인 화면의 복구 진입점을 사용한다. +2. 수신한 link/code의 대상 계정과 만료 여부를 확인한다. +3. 새 비밀번호 설정 뒤 기존 session이 유지되는지 화면 안내를 따른다. +4. 이미 사용했거나 만료된 복구 요청은 새로 발급한다. + +인증·로그인 정책의 정본은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow)다. + +## TOTP와 recovery code + +- 보안 설정 화면에서 Kratos TOTP를 등록·해제하고 recovery code를 발급한다. TOTP secret은 계정당 하나이며 RP별 secret을 만들지 않는다. +- TOTP 등록은 서로 다른 30초 구간에 표시된 코드 두 개를 연속으로 확인해야 완료된다. 첫 코드 직후에는 다음 코드로 바뀔 때까지 기다린 뒤 두 번째 값을 입력한다. +- recovery code는 TOTP의 평상시 대체 수단이 아니라 계정 복구에만 사용한다. 발급 직후 안전한 장소에 보관하고, 노출되었거나 사용한 code는 다시 발급한다. +- recovery code로 TOTP를 해제한 session은 새 TOTP를 등록하고 두 번째 코드까지 확인하기 전에는 비밀번호 변경, RP, AdminFront, DevFront, token 경로로 이동할 수 없다. +- 이 복구는 lookup secret을 Kratos에 제출하기 전에 session 한정 `lookup_pending` 상태로 서버에 먼저 기록된다. 원장을 기록할 수 없으면 lookup secret도 제출하지 않는다. 명시적인 잘못된 code 응답은 pending을 취소하고, 응답 결과가 불명확하면 해당 session만 격리한다. Kratos가 성공을 확인한 뒤에만 `lookup_self_recovery`를 `session_bound`로 승격하고 같은 identity의 다른 session을 즉시 fence한다. 복구를 시작한 동일 session만 새 TOTP 등록과 recovery code 재발급을 끝낼 수 있다. +- Tenant 또는 RP가 AAL2를 요구하는데 현재 session이 부족하면 UserFront가 Kratos AAL2 로그인 화면으로 이동시킨다. Factor 검증이 끝나면 원래 로그인 흐름을 이어간다. +- 비밀번호 reset 또는 TOTP reset 대상 계정은 최초 정상화 session에서 비밀번호 변경, TOTP 등록, recovery code 확인을 지정된 순서대로 완료한다. 이 흐름이 끝나기 전에는 일반 제품·RP·token 경로를 사용할 수 없다. +- 일반 보안 설정의 비밀번호 변경, TOTP 초기화, recovery code 재발급, 신뢰할 수 있는 브라우저 등록은 작업 직전에 현재 TOTP를 다시 검증한다. recovery code로 얻은 AAL2는 이 fresh-TOTP 확인을 대신할 수 없다. + +## 신뢰할 수 있는 브라우저 + +- AAL2 session에서 현재 브라우저를 등록할 수 있다. private key는 브라우저의 Web Crypto와 IndexedDB에 non-extractable key로 보관하며 서버에는 public JWK만 저장한다. +- grant는 최대 30일이며 현재 브라우저에서 자동 proof를 수행한다. 폐기, 만료, key 손실 또는 replay 검출 시 다시 등록해야 한다. +- 신뢰할 수 있는 브라우저만으로 Tenant AAL2를 유예하지 않는다. Connect 후보의 내부망 Network Assurance까지 같은 요청에서 성공해야 하며, RP AAL2·Fresh TOTP·Exact Super Admin 요구는 유예하지 않는다. diff --git a/docs/sso_guides/works-hr.md b/docs/sso_guides/works-hr.md new file mode 100644 index 0000000..9b70253 --- /dev/null +++ b/docs/sso_guides/works-hr.md @@ -0,0 +1,57 @@ +# WORKS 인사 연동 가이드 + +## 적용 범위 + +특수 인사 lifecycle과 WORKS 계정 자동화는 정책에 허용된 `hanmac-family` subtree에 적용된다. 인사 상태의 기준은 Baron이며 WORKS는 예약과 상태에 따라 생성·갱신·정지·삭제되는 외부 계정이다. + +## 상태 + +| 상태 | WORKS 처리 | Baron 처리 | +| --- | --- | --- | +| `active` | 유지·갱신 | 활성 | +| `temporary_leave` | 유지·갱신 | 단기휴무 | +| `suspended` | 정지 | 정지 | +| `preboarding` | 입사 예약 시각부터 생성 | 먼저 생성, 처리 후 활성 | +| `baron_guest` | 삭제 | 유지 | +| `offboarding` | 퇴사 예약 시각부터 삭제 | 처리 후 보관 | +| `extended_leave` | 삭제, 복귀 예약 시 재생성 | 복귀 후 활성 | +| `worksmobile_disabled` | 삭제 | 유지 | +| `archived` | 삭제 | 보관 | + +정확한 허용 상태와 예약 시각은 원격 Wiki 정책을 따른다. + +`active` 사용자의 WORKS 계정이 현재 없더라도 화면 조회만으로 상태를 바꾸지 않는다. 관리자가 생성 이력, 전체 Directory 조회 결과와 진행 중 작업을 확인한 뒤 `Baron WORKS 미사용으로 교정`을 실행한다. 생성 이력 확인 불가와 현재 Directory 조회 불가는 서로 다른 결과이며, 후자는 보정을 막는다. 상세 기준은 [WORKS 계정 이력·현재 부재 보정 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/WORKS-Account-Status-Reconciliation.-)을 따른다. + +## 사용자 추가와 상태 변경 + +1. AdminFront 사용자 화면에서 대상 Tenant를 선택한다. +2. 즉시 사용자는 활성, 예약 입사자는 입사대기와 입사일을 지정한다. +3. 상태 변경에 필요한 날짜를 입력한다. +4. 저장 후 Baron 상태와 예약 작업이 표시되는지 확인한다. +5. WORKS 처리 실패 시 같은 작업을 반복 생성하지 않고 Queue/operation 상태를 확인한다. + +## WORKS에서 정보 가져오기 + +1. Worksmobile 비교 화면을 연다. +2. 대상 사용자의 `Works정보 가져오기`를 선택한다. +3. 차이가 있는 허용 field만 선택한다. +4. stale comparison이 거부되면 새로고침 후 다시 비교한다. +5. WORKS 값이 비어 있다는 이유로 Baron 값을 자동 삭제하지 않는다. + +수동 Works→Baron과 자동 Baron→Works 방향을 구분한다. 양방향 field는 충돌 정책을 확인한다. + +## 식별자와 값 변환 + +- 사용자 연결은 canonical UUID와 검증된 WORKS external key를 사용한다. +- email local-part uniqueness는 적용 Tenant subtree 정책을 따른다. +- phone은 WORKS outbound 형식으로 정규화하되 Baron 원본을 임의 변경하지 않는다. +- organization, grade, position, employee number의 source 방향을 field별로 유지한다. + +## 권한과 환경 + +- 메뉴 조회, WORKS 관리, 대상 Tenant 사용자 편집 권한을 각각 검사한다. +- staging에서는 조회·비교를 검증할 수 있어도 실제 WORKS write가 차단될 수 있다. +- production relay는 idempotency, retry, 예약 기록을 사용한다. +- rate limit은 bounded retry와 backoff를 사용하며 무제한 병렬 호출하지 않는다. + +테넌트 변경 후 반영 확인과 Directory·SCIM 연결 점검은 [WORKS 연동 Runbook](../runbooks/worksmobile.md)을 따른다. orphan 등 공통 복구는 [Data Integrity Runbook](../runbooks/data-integrity.md)을 따른다. 정책 정본은 [Gitea Wiki](https://gitea.hmac.kr/baron/baron-sso/wiki)다.