This commit is contained in:
1 parent
b7120d448c
commit
c87e0aa0c1
16 files changed
+5559
No files matched your search
@@ -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 직접 조회.
|
||||
@@ -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의 대기·실패 항목에서 재시도 중단을 선택하고 확인하십시오. 중단한 항목은 기록과 선행 관계를 유지하며 자동·수동 재시도에서 제외됩니다. 중단 수는 별도로 표시합니다. 이미 처리 중이거나 완료된 작업은 중단할 수 없습니다. 선행 작업을 중단하면 후속 작업이 계속 대기할 수 있습니다.
|
||||
|
||||
중단을 성공 처리로 간주하지 않습니다. 이 화면은 중단 해제나 전체 큐 삭제를 제공하지 않습니다.
|
||||
@@ -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)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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)를 따른다.
|
||||
@@ -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 키의 접근 범위는 바뀌지 않습니다.
|
||||
@@ -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: <read-client-id>
|
||||
X-Baron-Key-Secret: <read-secret>
|
||||
```
|
||||
|
||||
응답의 `revision`은 조회한 사용자 정보의 버전입니다. 이 값을 `expectedRevision`에 넣고 수정 키로 변경을 요청합니다. 사번 앞자리의 0을 보존하려면 JSON 문자열로 전달합니다.
|
||||
|
||||
```http
|
||||
PATCH /api/v1/integrations/hr/users/{userId}/profile
|
||||
X-Baron-Key-ID: <update-client-id>
|
||||
X-Baron-Key-Secret: <update-secret>
|
||||
Idempotency-Key: <unique-source-event-id>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"expectedRevision": "<revision-from-get>",
|
||||
"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: <read-client-id>
|
||||
X-Baron-Key-Secret: <read-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 값이 표시될 수 있습니다.
|
||||
@@ -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입니다.
|
||||
|
||||

|
||||
|
||||
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='<SUPER_ADMIN_ACCESS_TOKEN>'
|
||||
SCOPE_TENANT_ID='11111111-1111-4111-8111-111111111111'
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--request POST \
|
||||
"${BASE_URL}/api/v1/admin/integration-credentials" \
|
||||
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data "{
|
||||
\"name\": \"Example HR directory user creator\",
|
||||
\"operationIds\": [\"directory.user.create\"],
|
||||
\"scope\": {
|
||||
\"kind\": \"tenant_subtree\",
|
||||
\"tenantId\": \"${SCOPE_TENANT_ID}\"
|
||||
},
|
||||
\"exposure\": \"structure\",
|
||||
\"expiresInDays\": 90,
|
||||
\"reason\": \"외부 사용자 계정 연동\"
|
||||
}"
|
||||
```
|
||||
|
||||
응답 예시는 다음과 같습니다. `clientSecret`은 이 응답에서만 한 번 표시됩니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"credential": {
|
||||
"id": "22222222-2222-4222-8222-222222222222",
|
||||
"name": "Example HR directory user creator",
|
||||
"clientId": "bic_<EXAMPLE_KEY_ID>",
|
||||
"operationIds": ["directory.user.create"],
|
||||
"scopeKind": "tenant_subtree",
|
||||
"scopeTenantId": "11111111-1111-4111-8111-111111111111",
|
||||
"exposure": "structure",
|
||||
"status": "active",
|
||||
"creationReason": "외부 사용자 계정 연동",
|
||||
"createdAt": "2026-08-24T00:00:00Z"
|
||||
},
|
||||
"clientSecret": "<DISPLAYED_ONCE_CLIENT_SECRET>"
|
||||
}
|
||||
```
|
||||
|
||||
Directory Operation에서는 `exposure` 입력값과 관계없이 서버가 `structure`로 고정합니다. `global` scope는 허용하지 않습니다. `operationIds`에는 정확히 하나의 OperationID만 넣어야 합니다.
|
||||
|
||||
관리 API에서 가능한 credential 작업은 다음과 같습니다.
|
||||
|
||||
| 목적 | Method 및 endpoint | 동작 |
|
||||
| --- | --- | --- |
|
||||
| 목록 | `GET /api/v1/admin/integration-credentials` | Secret을 제외한 발급 이력을 조회합니다. |
|
||||
| 신규 발급 | `POST /api/v1/admin/integration-credentials` | 새 credential과 1회 표시 Secret을 발급합니다. |
|
||||
| Secret 교체 | `POST /api/v1/admin/integration-credentials/{id}/rotate` | 기존 Secret을 즉시 무효화하고 새 Secret을 한 번 반환합니다. |
|
||||
| 즉시 회수 | `POST /api/v1/admin/integration-credentials/{id}/revoke` | credential을 `revoked`로 변경하고 Secret hash를 제거합니다. |
|
||||
| legacy 재발급 | `POST /api/v1/admin/integration-credentials/{id}/reissue` | `inactive` legacy 이력만 새 최소 권한과 새 Secret으로 재발급합니다. |
|
||||
|
||||
근거: [OpenAPI credential 계약](../../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 | `+`와 국가번호를 포함하며 공백·하이픈을 넣지 않습니다. 형식 예: `+<COUNTRY_CODE><NATIONAL_NUMBER>` |
|
||||
|
||||
### 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_<EXAMPLE_KEY_ID>'
|
||||
BARON_KEY_SECRET='<EXAMPLE_KEY_SECRET>'
|
||||
PRIMARY_TENANT_ID='11111111-1111-4111-8111-111111111111'
|
||||
IDEMPOTENCY_KEY='33333333-3333-4333-8333-333333333333'
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--request POST \
|
||||
"${BASE_URL}/api/v1/integrations/directory/users" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header "X-Baron-Key-ID: ${BARON_KEY_ID}" \
|
||||
--header "X-Baron-Key-Secret: ${BARON_KEY_SECRET}" \
|
||||
--header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \
|
||||
--data "{
|
||||
\"email\": \"external.user@example.test\",
|
||||
\"name\": \"Example External User\",
|
||||
\"primaryTenantId\": \"${PRIMARY_TENANT_ID}\"
|
||||
}"
|
||||
```
|
||||
|
||||
위 예시는 PII로 오인될 수 있는 전화번호를 의도적으로 넣지 않았습니다. `phone`이 필요하면 compact E.164 값으로 추가하고, 필요하지 않으면 해당 key 전체를 생략합니다. 빈 문자열을 보내는 방식보다 필드를 생략하는 편이 호출 계약을 명확하게 유지합니다.
|
||||
|
||||
근거: [OpenAPI `DirectoryUserCreateRequest`](../../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='<AUTHORIZED_ADMIN_ACCESS_TOKEN>'
|
||||
USER_ID='44444444-4444-4444-8444-444444444444'
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
"${BASE_URL}/api/v1/admin/users/${USER_ID}" \
|
||||
--header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}"
|
||||
```
|
||||
|
||||
다음을 확인합니다.
|
||||
|
||||
- 응답 `id`가 생성 응답의 `userId`와 같습니다.
|
||||
- `email`과 `name`이 정규화된 생성 값과 같습니다.
|
||||
- `role`은 `user`, `status`는 `active`, `affiliationType`은 `external`입니다.
|
||||
- 대표 Tenant가 요청한 `primaryTenantId`입니다.
|
||||
|
||||
identity commit 뒤 projection이 지연될 수 있으므로 관리자 readback이 즉시 보이지 않으면 제한된 간격과 횟수로 다시 조회합니다. 새 `Idempotency-Key`로 사용자 생성을 반복하지 않습니다.
|
||||
|
||||
근거: [관리자 user read route](../../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_ID>'
|
||||
MEMBERSHIP_KEY_SECRET='<MEMBERSHIP_KEY_SECRET>'
|
||||
USER_ID='44444444-4444-4444-8444-444444444444'
|
||||
ADDITIONAL_TENANT_ID='55555555-5555-4555-8555-555555555555'
|
||||
MEMBERSHIP_IDEMPOTENCY_KEY='66666666-6666-4666-8666-666666666666'
|
||||
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--request PATCH \
|
||||
"${BASE_URL}/api/v1/integrations/directory/users/${USER_ID}/memberships" \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header "X-Baron-Key-ID: ${MEMBERSHIP_KEY_ID}" \
|
||||
--header "X-Baron-Key-Secret: ${MEMBERSHIP_KEY_SECRET}" \
|
||||
--header "Idempotency-Key: ${MEMBERSHIP_IDEMPOTENCY_KEY}" \
|
||||
--data "{
|
||||
\"addTenantIds\": [\"${ADDITIONAL_TENANT_ID}\"],
|
||||
\"removeTenantIds\": []
|
||||
}"
|
||||
```
|
||||
|
||||
PATCH는 대표 Tenant를 변경하지 않고 일반 소속 delta를 원자적으로 반영합니다. 현재와 결과 소속은 같은 최상위 Tenant root에 속해야 하며, 대표 Tenant 제거와 마지막 소속 제거는 허용하지 않습니다.
|
||||
|
||||
근거: [OpenAPI membership 계약](../../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)
|
||||
@@ -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를 따른다.
|
||||
@@ -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=<tenant-uuid>&exposure=members
|
||||
X-Baron-Key-ID: <integration-key-id>
|
||||
X-Baron-Key-Secret: <integration-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-<build-id>.js`, `boc-<build-id>.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
|
||||
<div id="api-org-picker"></div>
|
||||
<pre id="api-org-picker-result" aria-live="polite"></pre>
|
||||
```
|
||||
|
||||
```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 코드를 `<script type="module">` 안에 넣습니다. `renderOrgChart()`는 호출하지 않아도 됩니다. `renderOrgPicker()`는 넘겨받은 model로 검색·선택 UI를 만들며 직접 API를 조회하지 않습니다. 기본 스타일은 자동 삽입됩니다. 스타일을 직접 제공할 때만 `injectStyles: false`를 사용합니다.
|
||||
|
||||
SDK 옵션은 `selectable: "tenant" | "user" | "both"`입니다. iframe query의 `select`와 이름이 다릅니다. `variant: "orgfront"`는 OrgFront 형태의 UI preset이며, `/embed/picker` 페이지 자체를 실행하는 옵션은 아닙니다.
|
||||
|
||||
콜백의 `selection`은 `{ type, id, name }[]`이고 `user.id`는 canonical identity UUID입니다. 같은 정보를 DOM 이벤트 `baron-org-picker-change`/`baron-org-picker-confirm`의 `event.detail.selection`으로도 받을 수 있습니다. SDK 방식에는 iframe의 `orgfront:picker:*`, `context`, nonce 검증을 적용하지 않습니다.
|
||||
|
||||
반환된 controller는 `getSelection()`, `confirm()`, `cancel()`, `destroy()`를 제공합니다. `confirm()`/`cancel()`은 이벤트를 발생시키고, 화면 제거는 `destroy()`로 처리합니다. 데이터가 변경되면 기존 picker를 `destroy()`한 뒤 새 응답으로 model과 picker를 다시 만듭니다.
|
||||
|
||||
## 1. iframe 방식 연동 준비
|
||||
|
||||
운영 담당자에게 OrgFront 공개 URL과 사용자의 조직 조회 권한을 확인합니다. 아래 예제의 `https://org.example.com`은 실제 주소로 바꿉니다. iframe을 넣을 서비스도 HTTPS로 제공합니다. 로컬 검증에는 HTTP localhost를 사용할 수 있습니다.
|
||||
|
||||
| 경로 | 용도 |
|
||||
| --- | --- |
|
||||
| `/embed/picker` | 외부 화면에 넣는 선택기 |
|
||||
| `/login?auto=1&returnTo=...` | OrgFront 인증을 거쳐 선택기로 진입 |
|
||||
| `/picker` | 선택 모드와 대상을 바꿔 보는 화면 |
|
||||
| `/embed-preview` | iframe URL과 선택 메시지를 확인하는 화면 |
|
||||
|
||||
부모 서비스의 로그인 상태만으로 OrgFront 로그인이 완료되지는 않습니다. OrgFront 세션이 있으면 선택기로 이동하고, 없으면 `auto=1` 진입이 OIDC 자동 로그인을 시도합니다. 자동 인증이 실패하면 대화형 로그인 재시도 또는 로그인 화면으로 이어질 수 있습니다. iframe 안에서 추가 로그인 없이 항상 열린다고 가정하지 마세요.
|
||||
|
||||
실제 외부 도메인에서 로그인 진입을 확인합니다. iframe이 차단되면 부모의 `frame-src`, OrgFront·로그인 화면의 `frame-ancestors`/`X-Frame-Options`, 브라우저의 쿠키 제한을 확인합니다. 로그인 화면을 임베딩할 수 없는 배포에서는 먼저 OrgFront를 최상위 창에서 열어 로그인한 뒤 선택기를 다시 엽니다. 이 절차만으로 브라우저의 쿠키 제한이 해제되는 것은 아닙니다.
|
||||
|
||||
현재 인증·세션 운영 정책은 [Authentication and Login Flow](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow)를 따릅니다.
|
||||
|
||||
## 2. 선택기 URL 만들기
|
||||
|
||||
사용자 다중선택의 기본 조합은 다음과 같습니다. URL은 문자열을 직접 이어 붙이지 말고 `URLSearchParams`로 만듭니다.
|
||||
|
||||
```js
|
||||
const orgOrigin = new URL("https://org.example.com").origin;
|
||||
const rootTenantId = "all";
|
||||
const nonce = crypto.randomUUID(); // 열 때마다 새로 생성
|
||||
const params = new URLSearchParams({
|
||||
mode: "multiple",
|
||||
select: "user",
|
||||
rootTenantId,
|
||||
parentOrigin: window.location.origin,
|
||||
requestNonce: nonce,
|
||||
includeDescendants: "true",
|
||||
showDescendantToggle: "true",
|
||||
});
|
||||
const pickerPath = `/embed/picker?${params}`;
|
||||
const loginUrl = new URL("/login", orgOrigin);
|
||||
loginUrl.search = new URLSearchParams({
|
||||
auto: "1",
|
||||
returnTo: pickerPath,
|
||||
}).toString();
|
||||
```
|
||||
|
||||
`returnTo`는 OrgFront 내부 상대 경로입니다. RP callback 주소를 넣지 않습니다. `parentOrigin`은 선택기를 담는 부모 페이지의 origin이며 경로나 마지막 `/`를 붙이지 않습니다. 예: `https://app.example.com`.
|
||||
|
||||
### URL 옵션
|
||||
|
||||
아래 기본값은 `/embed/picker`에 직접 접근했을 때 기준입니다. `/picker`와 `/embed-preview`의 URL 생성기는 모드를 명시하므로 외부에서도 `mode`와 `select`를 명시하세요.
|
||||
|
||||
| 옵션 | 값과 현재 동작 |
|
||||
| --- | --- |
|
||||
| `mode` | `single` 또는 `multiple`. 직접 접근 시 생략하면 `single` |
|
||||
| `select` | `tenant`, `user`, `both`. 생략하면 `both` |
|
||||
| `rootTenantId` | 루트 Tenant ID 또는 slug. `all`은 조회된 트리의 루트들을 표시하며 독립 `USER_GROUP` 루트는 제외. 특정 범위는 아래 allowlist 설명 참고 |
|
||||
| `parentOrigin` | 부모의 정확한 origin. 생략하거나 잘못된 값이면 현재 구현은 `*`로 전송하므로 반드시 명시 |
|
||||
| `requestNonce` | 이번 열기 요청을 식별하는 임의 문자열. `rootTenantId`와 함께 있어야 응답 `context`가 생성됨 |
|
||||
| `tenantSlug` | 선택한 루트 안에서 표시할 하위 Tenant. `tenantId`, `companyTenantId`도 호환 별칭으로 수용하며 이 순서로 우선 적용 |
|
||||
| `allowedRootTenantIds` | 표시할 관리 루트 ID를 쉼표로 구분. 지정하면 `rootTenantId`/하위 Tenant 선택보다 우선. 빈 값 또는 `__none__`은 빈 트리 |
|
||||
| `allowedTenantIds` | 선택 가능한 Tenant ID의 allowlist. 빈 값 또는 `__none__`은 빈 목록. 표시 트리도 필터링하므로 사용자 표시 범위에 영향을 줄 수 있음 |
|
||||
| `includeInternal` | `true`이면 내부 Tenant 포함. 기본 `false` |
|
||||
| `includePrivate` | `true`이면 비공개 Tenant 포함. 기본 `false` |
|
||||
| `includeDescendants` | 다중선택에서 하위 항목도 선택. 기본 `true` |
|
||||
| `showDescendantToggle` | 하위 선택 체크박스 표시. 기본 `true` |
|
||||
| `token` | 이미 발급된 공유 조직도 토큰으로 공개 조회 경로 사용. 아래 공유 모드 참고 |
|
||||
|
||||
`rootTenantId`를 생략하거나 찾지 못하면 현재 구현은 `hanmac-family` 회사그룹, 첫 회사그룹, 부모 없는 Tenant 순으로 기본 루트를 찾습니다. 특정 범위를 반드시 지정해야 하면 Backend가 승인한 `allowedRootTenantIds`를 전달하고, 결과 저장 시 Backend에서 다시 권한을 검사합니다. `rootTenantId=all`도 서버 조회 권한을 늘리지는 않습니다.
|
||||
|
||||
`tenantSlug`에 의한 하위 범위 지정은 `rootTenantId=all` 또는 `allowedRootTenantIds`로 루트를 구성하는 경우에는 적용되지 않습니다. 한 번에 한 가지 범위 지정 방식을 사용하세요.
|
||||
|
||||
`width`/`height`는 검증 화면의 URL 생성 옵션에도 보이지만, 실제 iframe 크기는 부모의 CSS로 지정합니다. 선택기 내부는 iframe의 화면 높이를 사용합니다.
|
||||
|
||||
## 3. 선택 결과 수신
|
||||
|
||||
선택기는 부모에게 `window.parent.postMessage`를 보냅니다. 부모는 **origin, iframe 창, nonce, rootTenantId, payload 형식**을 검사한 뒤 사용합니다. `origin`만 확인하면 같은 OrgFront의 다른 iframe에서 보낸 메시지도 받아들일 수 있습니다.
|
||||
|
||||
| 메시지 `type` | 의미 |
|
||||
| --- | --- |
|
||||
| `orgfront:picker:ready` | 선택기 컴포넌트가 마운트됨. 조직 데이터 조회 완료나 인증 성공 보증이 아님 |
|
||||
| `orgfront:picker:confirm` | 사용자가 **선택 완료**를 누름. `payload.mode`, `payload.selections` 포함 |
|
||||
| `orgfront:picker:cancel` | 사용자가 선택기 내부 **취소**를 누름. 부모가 iframe/모달을 닫아야 함 |
|
||||
| `orgfront:picker:error` | 조직 데이터 조회 실패. `error: "org_picker_load_failed"` |
|
||||
|
||||
`requestNonce`와 `rootTenantId`를 함께 보낸 경우 위 메시지에 다음 `context`가 포함됩니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "orgfront:picker:confirm",
|
||||
"context": { "nonce": "이번 요청의 nonce", "rootTenantId": "all" },
|
||||
"payload": {
|
||||
"mode": "multiple",
|
||||
"selections": [
|
||||
{
|
||||
"type": "user",
|
||||
"id": "11111111-1111-4111-8111-111111111111",
|
||||
"name": "김하나 책임",
|
||||
"email": "one@example.com",
|
||||
"rootTenantName": "개발본부",
|
||||
"leafTenantName": "개발팀"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
항목의 필수 필드는 `type`, `id`, `name`입니다. `type`은 `tenant` 또는 `user`이며, 사용자 항목에는 이메일과 조직 표시명이 추가될 수 있습니다. 이름에는 직급 등 표시용 텍스트가 포함될 수 있으므로 이름이나 이메일로 사용자를 식별하지 않습니다. **사용자 연결·저장에는 `id`의 Baron canonical identity UUID를 사용합니다.**
|
||||
|
||||
선택 완료는 조회 결과 전달입니다. RP 사용자 등록이나 whitelist 저장 API를 자동 호출하지 않습니다. 부모가 ID를 자신의 Backend에 제출하고, Backend는 대상의 활성 상태·소속·처리 권한을 다시 검사해야 합니다. URL allowlist나 nonce는 권한 증명서가 아닙니다.
|
||||
|
||||
## 4. 부모 페이지 예제
|
||||
|
||||
2절 코드를 먼저 실행한 뒤 아래 예제를 이어 붙입니다. `org-picker-host`와 `org-picker-result` 요소가 있는 부모 페이지에서 동작합니다. 사용자 다중선택 전용이며, 조직 선택으로 바꿀 때는 `select`, 응답 `type`, ID 검증도 함께 바꿉니다.
|
||||
|
||||
```html
|
||||
<div id="org-picker-host"></div>
|
||||
<pre id="org-picker-result" aria-live="polite"></pre>
|
||||
```
|
||||
|
||||
```js
|
||||
const host = document.getElementById("org-picker-host");
|
||||
const result = document.getElementById("org-picker-result");
|
||||
const frame = document.createElement("iframe");
|
||||
frame.title = "사용자 다중선택";
|
||||
frame.style.cssText = "width:100%;height:480px;border:0";
|
||||
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
let closed = false;
|
||||
|
||||
function closePicker() {
|
||||
closed = true;
|
||||
clearTimeout(readyTimer);
|
||||
window.removeEventListener("message", receive);
|
||||
frame.remove();
|
||||
}
|
||||
|
||||
function receive(event) {
|
||||
if (closed || event.origin !== orgOrigin || event.source !== frame.contentWindow) return;
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== "object" ||
|
||||
data.context?.nonce !== nonce || data.context?.rootTenantId !== rootTenantId) return;
|
||||
|
||||
switch (data.type) {
|
||||
case "orgfront:picker:ready":
|
||||
clearTimeout(readyTimer);
|
||||
result.textContent = "선택기가 열렸습니다. 데이터 로딩 상태는 선택기에서 확인하세요.";
|
||||
break;
|
||||
case "orgfront:picker:cancel":
|
||||
result.textContent = "선택을 취소했습니다.";
|
||||
closePicker();
|
||||
break;
|
||||
case "orgfront:picker:error":
|
||||
result.textContent = "조직을 불러오지 못했습니다. 조회 권한과 연결 상태를 확인하세요.";
|
||||
closePicker();
|
||||
break;
|
||||
case "orgfront:picker:confirm": {
|
||||
const items = data.payload?.selections;
|
||||
if (data.payload?.mode !== "multiple" || !Array.isArray(items) || !items.length) return;
|
||||
if (!items.every(item => item && item.type === "user" &&
|
||||
typeof item.id === "string" && uuid.test(item.id) && typeof item.name === "string")) return;
|
||||
const selected = [...new Map(items.map(item => [item.id, { id: item.id, name: item.name }])).values()];
|
||||
// 이 ID 목록을 자체 Backend에 제출하고 별도로 저장 결과를 처리합니다.
|
||||
result.textContent = JSON.stringify(selected, null, 2);
|
||||
closePicker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", receive); // iframe을 열기 전에 등록
|
||||
const readyTimer = setTimeout(() => {
|
||||
result.textContent = "선택기 응답이 없습니다. OrgFront 로그인과 iframe 허용 설정을 확인하세요.";
|
||||
closePicker();
|
||||
}, 20000); // 예제의 대기 시간이며 서비스 응답 조건에 맞게 조정
|
||||
frame.src = loginUrl.toString();
|
||||
host.append(frame);
|
||||
// 부모 모달을 닫거나 페이지를 떠날 때도 closePicker() 호출
|
||||
```
|
||||
|
||||
예제의 20초 대기 시간에는 OrgFront 로그인 시간도 포함됩니다. 대화형 로그인 중에도 시간이 지나면 iframe이 닫히므로, 실제 서비스에서는 로그인 절차를 먼저 완료하거나 로그인에 필요한 대기 시간으로 조정합니다.
|
||||
|
||||
선택기 내부에는 검색과 선택 완료 UI가 있습니다. 부모에서 별도 저장 확인이 필요하면 선택 결과를 보관해 확인 화면을 보여준 뒤 저장합니다. 반복해서 여는 모달에 적용할 때는 2·4절 JavaScript를 하나의 열기 함수 안에서 실행하고, 해당 함수에서 `closePicker`를 반환해 부모의 닫기 동작에 연결합니다. 다시 열 때는 이전 함수가 반환한 닫기 함수를 호출하고 새 nonce로 URL을 생성합니다. 현재 선택기에는 초기 선택 ID 주입이나 부모가 보내는 선택 변경 메시지 계약이 없습니다.
|
||||
|
||||
## 5. 공유 모드와 문제 확인
|
||||
|
||||
이미 발급된 공유 조직도 토큰을 사용하는 경우 `/embed/picker?token=...`에 나머지 옵션을 함께 전달합니다. 이 경로는 공개 조직도 조회를 사용하며 `includeInternal`과 `includePrivate`는 적용하지 않습니다. 공유 토큰을 RP access token이나 refresh token으로 대체하지 않습니다. [임베딩 토큰 설계](../../orgfront/docs/orgchart-embedding-token-design.md)에 나오는 권장 발급 API는 설계 제안이므로 이 가이드의 구현된 발급 절차로 취급하지 않습니다.
|
||||
|
||||
| 증상 | 확인할 항목 |
|
||||
| --- | --- |
|
||||
| 로그인 화면 또는 빈 iframe | 공개 URL, OrgFront 로그인, iframe 허용 헤더, 브라우저 쿠키 제한 |
|
||||
| `ready` 이후 오류 | OrgFront 조직도 API 응답과 현재 사용자의 조회 권한 |
|
||||
| 부모가 선택 결과를 무시함 | 정확한 origin, `event.source`, 이번 nonce, `rootTenantId`, `payload.mode` |
|
||||
| 조직·사용자가 보이지 않음 | Backend 조회 범위, root/allowlist 조합, 내부·비공개 표시 옵션, 사용자 활성 상태 |
|
||||
| 취소를 눌러도 모달이 남음 | 부모의 `orgfront:picker:cancel` 처리와 닫기 함수 연결 |
|
||||
| 테스트 iframe에서 한글이 깨짐 | 테스트 HTML의 `<meta charset="utf-8">`와 응답 인코딩 |
|
||||
|
||||
## 6. 구현과 검증 참고
|
||||
|
||||
- [독립 Picker SDK 구현](../../orgfront/src/sdk/org-context-chart/index.ts), [SDK 테스트](../../orgfront/src/sdk/org-context-chart/orgContextChart.test.ts)
|
||||
|
||||
- [메시지 전송·옵션 해석](../../orgfront/src/features/orgchart/routes/OrgPickerPage.tsx), [트리 범위 구성](../../orgfront/src/features/orgchart/pickerTree.ts), [항목 타입](../../orgfront/src/features/orgchart/pickerTypes.ts)
|
||||
- [인증 진입](../../orgfront/src/features/auth/LoginPage.tsx), [인증 callback 처리](../../orgfront/src/features/auth/AuthCallbackPage.tsx)
|
||||
- [DevFront 다중선택 연동 사례](../../devfront/src/features/clients/RPLoginMemberAddDialog.tsx)
|
||||
- [실제 선택기 브라우저 통합 테스트](../../devfront/tests/whitelist-real-picker.spec.ts): `cd devfront && pnpm exec playwright test --config playwright.picker.config.ts --workers=1`
|
||||
|
||||
이 테스트는 두 Frontend의 실제 화면과 선택 메시지를 사용하고, 인증 상태와 Backend 응답에는 fixture를 사용합니다. 실제 외부 도메인의 SSO·iframe 허용 여부는 대상 배포에서 별도로 확인합니다.
|
||||
@@ -0,0 +1,385 @@
|
||||
# Headless RP의 자체 Consent UI와 Baron UI 연동
|
||||
|
||||
Headless는 사용자가 보는 로그인·동의 화면을 RP가 제어하는 방식입니다. UserFront 페이지 방식은 Baron 화면으로 이동해서 같은 인증·동의 절차를 마칩니다. 두 방식 모두 최종 authorization code 교환과 token 검증이 필요하며, RP session은 RP가 만듭니다.
|
||||
|
||||
RP가 화면을 직접 디자인하려면 API를 사용하고, UserFront와 같은 구조·테마를 RP 페이지 안에 표시하려면 Baron Consent JS UI를 사용합니다. JS UI는 UserFront 페이지를 iframe으로 넣는 방식이 아니라 RP DOM에 동의 화면을 렌더링합니다.
|
||||
|
||||
구현과 검증 증거는 [#2104](https://gitea.hmac.kr/baron/baron-sso/issues/2104)에서 관리합니다. 이 가이드는 해당 코드 revision을 기준으로 하며 프로덕션 배포 여부는 이슈의 배포 상태를 별도로 확인합니다.
|
||||
|
||||
## 연동 방식 선택
|
||||
|
||||
| 방식 | RP가 구현할 부분 | 현재 상태 |
|
||||
| --- | --- | --- |
|
||||
| RP 자체 UI + Consent API | 로그인·동의 화면, RP Backend adapter, 로그인 transaction 연결 | 서버 간 headless consent facade와 adapter 예제를 사용합니다. |
|
||||
| Baron JS UI를 RP DOM에 탑재 | 자체 로그인 화면, container, adapter 연결, callback 설정 | 독립 ES/UMD 라이브러리로 UserFront 구조·테마를 탑재합니다. |
|
||||
| 중앙 UserFront로 이동 | 반환 URL로 브라우저 이동, RP callback | 기존 기본 흐름입니다. |
|
||||
|
||||
자체 UI와 제공 JS UI는 같은 Backend의 동의·인가 판정을 사용합니다. 화면 라이브러리는 동의 원장이나 RP session을 소유하지 않습니다. [인증 정책](https://gitea.hmac.kr/baron/baron-sso/wiki/Authentication-and-Login-Flow)과 [시스템 경계](../principles/system-and-data.md)를 따릅니다.
|
||||
|
||||
## 로그인부터 동의 완료까지
|
||||
|
||||
### UserFront 페이지 방식
|
||||
|
||||
RP에는 시작·callback 구현이 필요합니다. 비밀번호 입력, 추가 인증, 동의 화면은 중앙 UserFront가 제공합니다. Headless client assertion이나 자체 consent adapter를 RP에 구현하지 않습니다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as 사용자
|
||||
participant RP as RP 웹/Backend
|
||||
participant Hydra as Hydra Public
|
||||
participant UF as UserFront
|
||||
participant API as Baron Backend
|
||||
User->>RP: 로그인 시작
|
||||
RP-->>User: state/nonce/PKCE를 결속한 authorization URL
|
||||
User->>Hydra: authorization 요청
|
||||
Hydra-->>UF: login_challenge로 브라우저 이동
|
||||
User->>UF: 로그인과 필요한 추가 인증
|
||||
UF->>API: 로그인 처리
|
||||
API-->>UF: redirectTo
|
||||
UF->>Hydra: 브라우저 continuation
|
||||
Hydra-->>UF: consent_challenge로 이동
|
||||
UF->>API: GET /auth/consent
|
||||
alt 기존 동의로 자동 승인
|
||||
API-->>UF: redirectTo
|
||||
else 사용자 선택 필요
|
||||
API-->>UF: client/requested_scope/scope_details
|
||||
User->>UF: 승인 또는 거절
|
||||
UF->>API: POST /auth/consent/accept 또는 reject
|
||||
API-->>UF: redirectTo
|
||||
end
|
||||
UF->>Hydra: 브라우저 continuation
|
||||
Hydra-->>RP: callback code 또는 error + state
|
||||
RP->>RP: state 검증, 성공 code 교환·token 검증, session 생성
|
||||
```
|
||||
|
||||
### Headless 자체 UI 또는 제공 JS UI 방식
|
||||
|
||||
RP Backend는 로그인 transaction과 Hydra cookie 문맥을 유지합니다. 자체 UI와 제공 JS UI의 차이는 렌더러이며, Backend adapter와 consent 원장은 같습니다. Client assertion은 RP Backend에서만 생성합니다.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as 사용자
|
||||
participant UI as RP 자체 UI 또는 Baron JS UI
|
||||
participant RP as RP Backend adapter
|
||||
participant Hydra as Hydra Public
|
||||
participant API as Baron Backend
|
||||
User->>UI: RP 로그인 시작
|
||||
UI->>RP: 브라우저 session에 연결된 로그인 transaction
|
||||
RP->>Hydra: authorization, transaction별 cookie 문맥
|
||||
Hydra-->>RP: 로그인 Location의 login_challenge
|
||||
User->>UI: 로그인 정보 입력
|
||||
UI->>RP: 로그인 제출
|
||||
RP->>API: POST /auth/headless/password/login + consent_mode rp + assertion
|
||||
API-->>RP: redirectTo
|
||||
RP->>Hydra: 허용된 continuation만 manual follow
|
||||
Hydra-->>RP: 중앙 consent Location의 consent_challenge
|
||||
Note over RP: 중앙 화면은 fetch하지 않고 challenge를 transaction에 저장
|
||||
RP-->>UI: RP consent 화면과 opaque flow ID
|
||||
UI->>RP: load + session/CSRF
|
||||
RP->>RP: Origin/session/CSRF/만료 및 transaction 결속 검증
|
||||
RP->>API: POST /auth/headless/consent/info + 저장된 두 challenge + assertion
|
||||
API->>API: client와 서버 login context 및 세션 연결 검증
|
||||
alt 자동 승인
|
||||
API-->>RP: redirectTo
|
||||
else 동의 화면
|
||||
API-->>RP: client/requested_scope/scope_details
|
||||
RP-->>UI: 표시 정보
|
||||
User->>UI: 승인 또는 거절
|
||||
UI->>RP: 선택 scope 또는 reject + session/CSRF
|
||||
RP->>API: POST /auth/headless/consent/accept 또는 reject
|
||||
API-->>RP: redirectTo
|
||||
end
|
||||
RP->>Hydra: 같은 서버 cookie jar로 최종 continuation
|
||||
Hydra-->>RP: RP callback Location, code 또는 error + state
|
||||
RP->>RP: callback origin/path와 state 검증
|
||||
RP-->>UI: 검증된 RP callback URL
|
||||
UI->>RP: 브라우저 callback 이동
|
||||
RP->>RP: state 검증, 성공 code 교환·token 검증, session 생성
|
||||
```
|
||||
|
||||
이 서버 중계 방식은 authorization 시작부터 최종 continuation까지 같은 transaction의 서버 cookie jar를 사용합니다. RP callback 직전까지 중계하고 origin/path·state를 검증한 다음 브라우저를 RP callback으로 이동시킵니다. 서로 다른 사용자의 cookie jar를 공유하지 않습니다.
|
||||
|
||||
1. RP는 authorization 요청마다 `state`, `nonce`, PKCE verifier/challenge를 만들고 해당 브라우저 transaction에 연결합니다.
|
||||
2. RP Backend가 `login_challenge`와 서명한 `client_assertion`으로 `POST /api/v1/auth/headless/password/login`을 호출합니다. 설정·JWT 규칙은 [README](../../README.md)의 headless 항목을 따릅니다.
|
||||
3. 성공 응답의 `redirectTo`를 RP Backend가 같은 서버 cookie jar로 중계해 consent Location을 받습니다. 이는 최종 callback URL을 보장하지 않습니다. 아직 `code`가 없다는 이유로 password login을 반복하지 않습니다.
|
||||
4. Consent 진입 경로가 전달한 별도 `consent_challenge`로 아래 조회·승인·거절을 처리합니다.
|
||||
5. Consent 결과의 `redirectTo`도 RP Backend가 같은 jar로 중계합니다. 검증된 RP callback으로 브라우저를 이동시키고, 성공 code의 교환과 token 검증을 마친 뒤 RP session을 만듭니다. 오류 callback에도 `state`를 검증합니다.
|
||||
|
||||
전역 `URLS_CONSENT`의 기본값은 `${USERFRONT_URL}/consent`입니다. Headless RP는 서버에서 authorization continuation을 중계하고 이 Location의 challenge를 자신의 transaction에 연결합니다. Headless 설정이나 RP의 `/consent` 화면 추가만으로 Hydra가 RP 화면으로 이동시키지는 않습니다. 한 RP를 위해 전역 URL을 교체하지 않습니다.
|
||||
|
||||
## RP 자체 UI가 사용하는 현재 API
|
||||
|
||||
API의 정본은 [OpenAPI](../../backend/docs/openapi.yaml)와 [Headless Consent handler](../../backend/internal/handler/auth_handler_headless_consent.go)입니다.
|
||||
|
||||
### RP 서버 전용 API와 중앙 UserFront API 구분
|
||||
|
||||
| 단계 | RP Backend가 호출하는 API | 중앙 UserFront API |
|
||||
| --- | --- | --- |
|
||||
| 동의 정보·자동 승인 | `POST /api/v1/auth/headless/consent/info` | `GET /api/v1/auth/consent` |
|
||||
| 승인 | `POST /api/v1/auth/headless/consent/accept` | `POST /api/v1/auth/consent/accept` |
|
||||
| 거절 | `POST /api/v1/auth/headless/consent/reject` | `POST /api/v1/auth/consent/reject` |
|
||||
|
||||
RP 자체 동의는 먼저 headless password login body에 `consent_mode: "rp"`를 넣습니다. Baron은 로그인과 consent를 연결하는 client·subject·Kratos session 정보를 서버 내부에 최대 10분 보관합니다. 기존 중앙 consent 방식은 이 필드를 생략합니다. 현재 이 opt-in은 password login에 적용되며 phone/link 전용 UI의 확장은 별도 범위입니다.
|
||||
|
||||
RP 전용 세 API는 매번 아래 필드를 보냅니다. `client_assertion`의 audience는 **호출하는 endpoint 자신의 URL**이어야 합니다. Password login assertion을 그대로 재사용하지 않습니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "example-rp",
|
||||
"client_assertion": "<endpoint-specific-signed-jwt>",
|
||||
"login_challenge": "<original-login-challenge-stored-by-rp-backend>",
|
||||
"consent_challenge": "<consent-challenge-stored-by-rp-backend>"
|
||||
}
|
||||
```
|
||||
|
||||
승인에만 `grant_scope`를 추가합니다. 서버 간 facade에는 브라우저의 `Cookie`나 사용자 `Authorization`을 전달하지 않습니다. 원래 로그인 세션과 중앙 정책을 서버에서 다시 검증합니다. RP는 Hydra 내부 ID를 추측·재구성하지 않고 자신이 받은 원본 login challenge를 보관해 전송합니다.
|
||||
|
||||
아래는 중앙 API를 기준으로 공통 응답·동의 선택 semantics를 설명합니다. RP는 위 facade 경로와 필수 인증 필드를 사용합니다.
|
||||
|
||||
### 동의 정보 또는 자동 승인 결과 조회
|
||||
|
||||
```http
|
||||
GET /api/v1/auth/consent?consent_challenge=<url-encoded-consent-challenge>
|
||||
```
|
||||
|
||||
표준 URL parser로 challenge를 읽고 전송 시 인코딩합니다. Login challenge를 consent challenge 대신 사용하지 않습니다.
|
||||
|
||||
| 성공 응답 | UI 처리 |
|
||||
| --- | --- |
|
||||
| `redirectTo` 포함 | Backend가 이미 자동 승인했습니다. 브라우저를 이동시키고 추가 조회·accept를 하지 않습니다. |
|
||||
| `requested_scope`, `client`, 선택적 `scope_details` 포함 | 동의 화면을 표시하고 사용자 입력을 기다립니다. |
|
||||
|
||||
동의 화면 응답의 주요 필드는 다음과 같습니다. RP adapter는 내부 subject·challenge 등 렌더링에 필요하지 않은 필드를 브라우저에 전달하지 않습니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"client": { "client_id": "example-rp", "client_name": "업무 포털" },
|
||||
"requested_scope": ["openid", "profile"],
|
||||
"scope_details": {
|
||||
"openid": { "description": "로그인 세션 확인", "mandatory": true },
|
||||
"profile": { "description": "이름과 사용자 식별자", "mandatory": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
자동 승인 또는 승인·거절 명령의 성공 응답은 다음 형태입니다. Baron은 Hydra continuation을 반환하고, 이 가이드의 RP adapter는 같은 서버 jar로 이를 처리한 후 브라우저용 RP callback URL을 반환합니다.
|
||||
|
||||
```json
|
||||
{ "redirectTo": "<verified-continuation-or-rp-callback-url>" }
|
||||
```
|
||||
|
||||
이 GET은 자동 승인이라는 부작용이 있으므로 polling·prefetch·공유 cache 대상으로 사용하지 않습니다. `skip`만으로 완료를 추정하지 않고 `redirectTo` 응답으로 분기합니다.
|
||||
|
||||
애플리케이션 이름, `requested_scope`, `scope_details[scope].description`과 `mandatory`를 화면에 표시합니다. 설명은 HTML로 삽입하지 않습니다. 필수 scope는 구분하고 전체 거절 버튼도 제공합니다. 기존 중앙 UI는 `openid`를 필수로 유지하고 요청 scope를 기본 선택합니다. 제공 JS UI에서 다른 기본 선택 정책을 도입한다면 별도 정책 검토가 필요합니다.
|
||||
|
||||
### 사용자 승인
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/consent/accept
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"consent_challenge": "<consent-challenge>",
|
||||
"grant_scope": ["openid", "profile"]
|
||||
}
|
||||
```
|
||||
|
||||
사용자가 승인한 scope와 필수 scope를 명시적으로 제출합니다. 성공 응답의 `redirectTo`로 이동하며, 중복 클릭과 자동 재제출을 막습니다.
|
||||
|
||||
**현재 `grant_scope` 생략 또는 빈 배열은 전체 거절이 아닙니다.** 요청 scope가 유지될 수 있고 필수 scope는 서버에서 다시 병합됩니다. 전체 거절은 아래 API를 사용합니다.
|
||||
|
||||
### 사용자 거절
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/consent/reject
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"consent_challenge": "<consent-challenge>"
|
||||
}
|
||||
```
|
||||
|
||||
반환 `redirectTo`로 OIDC 거절 흐름을 마칩니다. 거절을 자동 재로그인 조건으로 사용하지 않습니다.
|
||||
|
||||
### RP Backend와 브라우저 경계
|
||||
|
||||
자체 UI는 RP의 same-origin Backend adapter를 통해 호출합니다. Adapter는 자신이 시작한 로그인 transaction에 연결된 challenge만 처리하고 `client.client_id`를 대조하며, 조회·승인·거절 모두 브라우저 결속과 CSRF 방어를 적용합니다. Baron은 자신이 생성한 Hydra login context와 내부 client·subject·session 결속을 대조하고 실제 세션·중앙 정책을 재검증합니다. RP 브라우저 transaction은 RP의 책임입니다.
|
||||
|
||||
`client_assertion`만으로 브라우저 결속까지 해결되지는 않습니다. 로그인 완료 전 RP에 token이 없는 시점에도 RP의 로그인 transaction을 별도로 유지하고 cookie 문맥·만료·일회 소비를 연결해야 합니다. Private key·client secret·조직도용 integration key는 JS에 넣지 않습니다. Ory Admin API를 RP 브라우저에서 직접 호출하지 않습니다.
|
||||
|
||||
GET도 자동 승인할 수 있으므로 adapter는 Baron 조회를 호출하기 **전에** 브라우저 transaction과 challenge의 결속을 검증해야 합니다. 조회 응답의 client 대조만으로 이미 발생한 자동 승인을 되돌릴 수는 없습니다.
|
||||
|
||||
## Baron 제공 JS UI를 RP에 탑재하기
|
||||
|
||||
[OrgFront 독립 SDK](../../orgfront/README.md)의 API·모델·DOM renderer 분리를 따릅니다. [Consent SDK](../../userfront/consent-sdk/src/index.ts)는 `createConsentClient`, `createFetchTransport`, `mountConsent`를 제공합니다. UI는 Shadow DOM으로 RP 스타일과 격리하고 UserFront의 카드·서비스 정보·scope 목록·승인·취소 확인 구조를 사용합니다.
|
||||
|
||||
### 빌드와 배포 파일
|
||||
|
||||
저장소 루트에서 기존 OrgFront 개발 의존성을 설치한 후 SDK만 빌드합니다. 애플리케이션 배포 flow는 변경하지 않습니다.
|
||||
|
||||
```sh
|
||||
npm --prefix orgfront ci
|
||||
npm --prefix userfront/consent-sdk run build
|
||||
npm --prefix userfront/consent-sdk test
|
||||
node userfront/consent-sdk/examples/rp-adapter.test.mjs
|
||||
```
|
||||
|
||||
`userfront/consent-sdk/dist`에 다음 파일을 생성합니다. RP가 승인된 버전을 자신의 정적 파일 경로에 복사해 사용하며 공개 npm/CDN 배포는 전제하지 않습니다.
|
||||
|
||||
| 산출물 | 용도 |
|
||||
| --- | --- |
|
||||
| `baron-consent.js`, `baron-consent.min.js` | ES module |
|
||||
| `baron-consent.umd.cjs`, `baron-consent.min.umd.cjs` | script 방식의 전역 `BaronConsent` |
|
||||
| `index.d.ts` | TypeScript 타입 |
|
||||
|
||||
JS에 스타일이 포함되어 별도 CSS 파일은 필요하지 않습니다. UserFront와 동일한 글꼴을 사용하려면 RP도 `Noto Sans KR`을 로드해야 합니다. 예를 들어 저장소 `userfront/assets/fonts`의 Regular·Bold woff2를 RP 정적 파일로 제공하고 다음 CSS를 적용합니다. 배포 경로와 라이선스 파일은 RP가 사용하는 파일에 맞춰 유지합니다.
|
||||
|
||||
```css
|
||||
@font-face {
|
||||
font-family: "Noto Sans KR";
|
||||
src: url("/vendor/baron-consent/0.1.0/NotoSansKR-Regular.woff2");
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Noto Sans KR";
|
||||
src: url("/vendor/baron-consent/0.1.0/NotoSansKR-Bold.woff2");
|
||||
font-weight: 700;
|
||||
}
|
||||
```
|
||||
|
||||
### ES module 방식
|
||||
|
||||
아래 `/vendor/...`는 RP가 위 산출물을 호스팅하는 예시 경로입니다. `rpFlowId`와 `csrfToken`은 RP Backend가 해당 브라우저 session에 연결하여 HTML에 안전하게 전달한 값입니다. Hydra challenge나 client assertion을 브라우저에 입력받는 값이 아닙니다.
|
||||
|
||||
```html
|
||||
<div id="consent"></div>
|
||||
<script type="module">
|
||||
import {
|
||||
createConsentClient, createFetchTransport, mountConsent,
|
||||
} from "/vendor/baron-consent/0.1.0/baron-consent.js";
|
||||
|
||||
const boot = JSON.parse(document.querySelector("#rp-consent-config").textContent);
|
||||
const client = createConsentClient({
|
||||
expectedClientId: boot.clientId,
|
||||
allowedRedirectOrigins: [location.origin],
|
||||
allowedRedirectPaths: { [location.origin]: ["/auth/callback"] },
|
||||
transport: createFetchTransport({
|
||||
baseUrl: new URL("/auth/consent", location.origin).href,
|
||||
headers: { "X-CSRF-Token": boot.csrfToken },
|
||||
timeoutMs: 10000,
|
||||
}),
|
||||
});
|
||||
const view = mountConsent(document.querySelector("#consent"), {
|
||||
client,
|
||||
challenge: boot.rpFlowId,
|
||||
locale: "ko",
|
||||
theme: "light",
|
||||
onRedirect: (url) => location.assign(url),
|
||||
onError: (error) => {
|
||||
// 비밀이나 응답 원문 없이 문의용 ID만 진단에 사용합니다.
|
||||
console.info("Consent failed", error.requestId ?? "unavailable");
|
||||
},
|
||||
});
|
||||
window.addEventListener("pagehide", () => view.destroy(), { once: true });
|
||||
// SPA는 해당 route의 cleanup에서도 view.destroy()를 호출합니다.
|
||||
</script>
|
||||
```
|
||||
|
||||
`#rp-consent-config`는 RP가 제공해야 하는 `application/json` 요소입니다. 서버가 JSON을 HTML에 삽입할 때 `<` 등을 이스케이프하여 script 종료 문자열을 데이터가 만들지 못하게 합니다. 허용 continuation은 실제 Hydra/RP callback origin과 exact path에 맞춰 서버·SDK 양쪽에 설정합니다.
|
||||
|
||||
### 일반 script 방식
|
||||
|
||||
같은 산출물을 JavaScript MIME type으로 호스팅합니다. `.cjs` 확장자도 다운로드 파일로 응답하지 않도록 RP 정적 서버를 설정합니다.
|
||||
|
||||
```html
|
||||
<script src="/vendor/baron-consent/0.1.0/baron-consent.umd.cjs"></script>
|
||||
<script>
|
||||
const { createConsentClient, createFetchTransport, mountConsent } = BaronConsent;
|
||||
// 위 ES module 예제와 같은 설정으로 client와 view를 만듭니다.
|
||||
</script>
|
||||
```
|
||||
|
||||
`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": "<opaque-rp-flow-id>",
|
||||
"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)에서 관리합니다.
|
||||
@@ -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를 따른다.
|
||||
@@ -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로<br/>장비를 관리해야 하나요?"}
|
||||
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["목적에 맞는 지원 유형으로 재설계하거나<br/>조합 지원 개발을 별도로 검토"]
|
||||
C -->|예| D["기본 Client 등록<br/>client_id·callback·scope·키 설정"]
|
||||
D --> E["유형별 RP 로그인 또는 M2M 기본 흐름 구현"]
|
||||
E --> F["선택 옵션의 RP 선행 구현<br/>검증·logout·adapter·device proof 등"]
|
||||
F --> G["RP endpoint·키 배치 후 Baron 설정 저장"]
|
||||
G --> J{"지원되는 추가 로그인 보안 옵션을<br/>사용하나요?"}
|
||||
J -->|예| H["선택한 옵션만 저장<br/>whitelist는 Tenant 저장·명단 구성이 선행"]
|
||||
J -->|아니요| I
|
||||
H --> I["정상·거절·재인증·로그아웃 확인<br/>설정 새로고침으로 저장값 확인"]
|
||||
```
|
||||
|
||||
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://<hydra-public-origin>/oauth2/introspect
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
token=<url-encoded-token>
|
||||
&client_id=<url-encoded-client-id>
|
||||
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
|
||||
&client_assertion=<url-encoded-signed-jwt>
|
||||
```
|
||||
|
||||
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": "<opaque-session-handle>",
|
||||
"sessionExpiresAt": "2026-09-21T09:15:00Z",
|
||||
"sessionRefreshCredential": "<opaque-recovery-credential>",
|
||||
"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: <signed-proof-jwt>
|
||||
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을 포함한 다른 응답을 폐기 완료로 간주하지 않습니다. 오프라인 폐기와 다중 프로세스 처리 순서는 앞 절을 따릅니다.
|
||||
@@ -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=<jwt>`를 전송한다. 현재 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)다.
|
||||
@@ -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 요구는 유예하지 않는다.
|
||||
@@ -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)다.
|
||||
Reference in new issue
Block a user