From 0ab2c01718d4836577764227b46c921f8f156b28 Mon Sep 17 00:00:00 2001 From: kyy Date: Fri, 19 Jun 2026 10:05:10 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=B1=84=EB=84=90=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=20=EB=AC=B8=EC=84=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/backchannel-logout-guide.md | 237 +++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 docs/backchannel-logout-guide.md diff --git a/docs/backchannel-logout-guide.md b/docs/backchannel-logout-guide.md new file mode 100644 index 00000000..48b1a5bb --- /dev/null +++ b/docs/backchannel-logout-guide.md @@ -0,0 +1,237 @@ +# Back-Channel Logout 통합 가이드 + +이 문서는 Baron SSO와 연동하는 RP들이 `Back-Channel Logout`을 어떻게 처리하는지 한 곳에서 정리합니다. + +핵심은 다음 두 가지입니다. + +1. **Baron SSO가 RP로 보내는 요청 형식은 공통입니다.** +2. **RP가 그 요청을 받아 세션을 찾고 지우는 내부 로직은 RP 유형에 따라 달라질 수 있습니다.** + +## 결론 + +Baron SSO는 모든 RP에 대해 같은 방식으로 `POST /backchannel-logout`를 보냅니다. + +- `Content-Type: application/x-www-form-urlencoded` +- body: `logout_token=` +- 검증용 공개키는 `GET /api/v1/auth/backchannel/jwks.json` + +차이는 Baron이 보낸 뒤 **RP 내부에서 세션을 어디서 찾고 어떻게 파기하느냐**입니다. + +- `PKCE RP` +- `server-side-app RP` +- `headless login RP`는 `PKCE` 기반 custom login UI 변형 + +## Polling 방식에 대한 정리 + +`polling`은 **OIDC Back-Channel Logout 표준 자체에는 없습니다.** + +공식 사양은 OP가 RP의 `back-channel logout URI`로 직접 `HTTP POST`를 보내고, 본문에 `logout_token`을 포함하는 방식만 정의합니다. OIDC Back-Channel Logout은 OP와 RP 사이의 direct back-channel communication이며, RP가 주기적으로 OP를 조회해서 로그아웃 여부를 받는 polling 모델은 표준 경로가 아닙니다. +공식 사양 근거: [`OpenID Connect Back-Channel Logout 1.0`](https://openid.net/specs/openid-connect-backchannel-1_0.html) [turn3view0]. + +따라서 polling을 지원하고 싶다면, 그건 `Back-Channel Logout`이 아니라 **커스텀 세션 재검증 또는 heartbeat**로 보는 게 맞습니다. + +### polling을 쓸 때의 의미 + +Polling을 도입하면 RP는 Baron SSO에 다음과 같은 식으로 주기적으로 확인합니다. + +1. 현재 RP 세션이 아직 유효한지 확인합니다. +2. Baron 세션이 만료되었거나 연동 해지되었으면 RP 로컬 세션을 지웁니다. +3. 유효하면 아무 작업도 하지 않고 다음 주기까지 기다립니다. + +이 방식은 다음과 같은 상황에서 유용할 수 있습니다. + +- RP가 외부에서 inbound `POST`를 받기 어려운 경우 +- 백채널 엔드포인트를 노출하기 어렵고, RP가 outbound 요청만 가능할 때 +- 로그아웃 즉시성보다 단순한 운영을 우선할 때 + +하지만 이 방식의 한계도 분명합니다. + +- 로그아웃이 실시간이 아니라 주기 지연을 가집니다. +- 요청량이 늘어납니다. +- RP가 자체적으로 polling 스케줄과 실패 복구를 관리해야 합니다. + +### polling 기반 로직 구성 예시 + +polling을 선택한다면 보통 아래 구조로 갑니다. + +```mermaid +sequenceDiagram + autonumber + participant RP as RP + participant Baron as Baron SSO + participant Store as RP Session Store + + loop 주기적 확인 + RP->>Baron: 세션 유효성 조회 요청 + Baron-->>RP: 유효 / 무효 응답 + alt 유효 + RP->>RP: 로컬 세션 유지 + else 무효 + RP->>Store: session destroy + RP->>RP: 세션 매핑 제거 + end + end +``` + +권장 구성은 다음과 같습니다. + +1. 로그인 시 `sid` 또는 `sub`를 RP 세션에 저장합니다. +2. 주기 작업이 Baron의 세션 유효성 또는 사용자 프로필 확인 API를 호출합니다. +3. Baron 세션이 무효면 RP가 로컬 세션을 삭제합니다. +4. 민감한 화면 진입 전에도 한 번 더 재검증할 수 있습니다. + +이 방식은 `backchannel logout`과는 별개로 설계하는 것이 좋습니다. + +## 공통 시퀀스 + +아래 흐름은 세 RP에 공통입니다. + +```mermaid +sequenceDiagram + autonumber + participant Baron as Baron SSO + participant RP as RP + participant JWKS as Baron Back-Channel JWKS + participant Store as RP Session Store + + Baron->>RP: POST /backchannel-logout\nlogout_token= + RP->>RP: logout_token 추출 + RP->>JWKS: JWKS로 서명 검증 + JWKS-->>RP: public key + RP->>RP: iss / aud / events / nonce / jti 검증 + RP->>RP: sid 또는 sub로 세션 탐색 + RP->>Store: session destroy + Store-->>RP: 삭제 완료 + RP->>RP: 세션 매핑 제거 + RP-->>Baron: 200 OK +``` + +## 공통 전송 규칙 + +Baron SSO에서 RP로 보내는 형식은 동일합니다. + +| 항목 | 값 | +| --- | --- | +| HTTP method | `POST` | +| Path | `/backchannel-logout` | +| Content-Type | `application/x-www-form-urlencoded` | +| Body | `logout_token=` | +| 검증 JWKS | `/api/v1/auth/backchannel/jwks.json` | + +전송 로직은 Baron 쪽에서 공통으로 처리됩니다. + +- [`backend/internal/service/backchannel_logout_service.go`](/home/kyy/workspace/baron-sso/backend/internal/service/backchannel_logout_service.go) +- [`backend/internal/handler/auth_handler.go`](/home/kyy/workspace/baron-sso/backend/internal/handler/auth_handler.go) + +## RP별 차이 + +세 RP는 모두 `logout_token`을 받아 검증하고 세션을 지운다는 점은 같습니다. +다만 세션이 만들어지는 시점과 저장 방식이 다릅니다. + +| 항목 | PKCE RP | server-side-app RP | headless login RP | +| --- | --- | --- | --- | +| 로그인 성격 | Authorization Code + PKCE | confidential client | PKCE 기반 custom login UI | +| 백채널 수신 endpoint | 필요 | 필요 | 필요 | +| 세션 저장 구조 | 앱 서버/BFF/브라우저 연동에 따라 다양 | 서버 세션 중심 | headless 로그인 이후 로컬 세션 바인딩 | +| `sid/sub` 매핑 | callback 이후 저장 | callback 이후 저장 | login 성공 이후 저장 | +| 세션 파기 방식 | 매핑된 session id 삭제 | 매핑된 session id 삭제 | 매핑된 session id 삭제 | +| 차이의 핵심 | 서버 endpoint가 없으면 처리 불가 | 서버 세션 구조와 잘 맞음 | 로그인 진입점만 다르고 로그아웃 처리는 공통 패턴 | + +## RP별 처리 설명 + +### PKCE RP + +PKCE RP는 브라우저 기반 로그인 흐름을 사용하지만, 백채널 로그아웃을 받으려면 **반드시 서버 endpoint**가 있어야 합니다. + +이유는 Baron이 브라우저가 아니라 RP 서버로 직접 `POST`를 보내기 때문입니다. + +처리 순서: + +1. callback 이후 `sid` 또는 `sub`를 RP 세션과 바인딩합니다. +2. Baron이 `POST /backchannel-logout`를 보냅니다. +3. RP가 `logout_token`을 검증합니다. +4. `sid` 우선, 실패 시 `sub`로 세션을 찾습니다. +5. 세션 스토어에서 해당 세션을 삭제합니다. + +주의: + +- 순수 frontend-only PKCE 앱은 백채널 로그아웃을 직접 받을 수 없습니다. +- 서버나 BFF가 있어야 합니다. + +### server-side-app RP + +server-side-app RP는 confidential client이므로, 서버 세션 구조와 백채널 로그아웃이 자연스럽게 맞습니다. + +처리 순서: + +1. OIDC Authorization Code 로그인과 callback을 처리합니다. +2. callback 이후 `sid` 또는 `sub`를 서버 세션과 바인딩합니다. +3. Baron이 `POST /backchannel-logout`를 보냅니다. +4. RP가 `logout_token`을 검증합니다. +5. 세션 매핑을 찾아 직접 파기합니다. + +이 유형은 PKCE보다 세션 관리가 명확해서 문서화와 운영이 단순합니다. + +### headless login RP + +headless login은 **별도의 로그아웃 타입이 아니라 PKCE 계열의 로그인 변형**입니다. + +즉, 로그인 시에는 custom login UI가 있고 RP backend가 headless login API를 호출하지만, 백채널 로그아웃은 결국 동일한 패턴으로 처리합니다. + +처리 순서: + +1. headless login 성공 후 `sid` 또는 `sub`를 RP 세션에 바인딩합니다. +2. Baron이 `POST /backchannel-logout`를 보냅니다. +3. RP가 `logout_token`을 검증합니다. +4. `sid` 또는 `sub`로 세션을 찾습니다. +5. 세션 스토어에서 해당 세션을 삭제합니다. + +핵심은 로그인 진입점만 다르고, 로그아웃 처리 패턴은 PKCE RP와 같습니다. + +## 공통 검증 규칙 + +RP는 아래 항목을 검증해야 합니다. + +1. JWT 서명 검증 +2. `iss`가 Baron OIDC issuer와 일치 +3. `aud`에 현재 RP `client_id` 포함 +4. `iat` 존재 +5. `jti` 존재 +6. `events`에 `http://schemas.openid.net/event/backchannel-logout` 포함 +7. `nonce`가 없어야 함 +8. `sid` 또는 `sub`가 있어야 함 + +권장 사항: + +- `jti` replay 방지 +- 시계 오차 허용 +- 검증 실패 시 `400` + +## 세션 파기 규칙 + +`Back-Channel Logout`은 현재 브라우저 요청의 `req.session.destroy()`만으로는 부족합니다. + +반드시 세션 저장소에서 실제 세션 id를 찾아 직접 파기해야 합니다. + +권장 우선순위: + +1. `sid`로 탐색 +2. `sid`가 없거나 매칭 실패 시 `sub`로 fallback + +## 공통 테스트 포인트 + +1. RP 로그인 후 `sid/sub -> sessionId` 매핑이 생성되는지 확인 +2. Baron이 `POST /backchannel-logout`를 실제로 보내는지 확인 +3. RP가 `logout_token`을 검증하는지 확인 +4. 세션 스토어에서 세션이 삭제되는지 확인 +5. 동일한 `logout_token` 재전송 시 replay 방지가 동작하는지 확인 + +## 관련 문서 + +- [`docs/pkce-backchannel-logout-guide.md`](/home/kyy/workspace/baron-sso/docs/pkce-backchannel-logout-guide.md) +- [`docs/server-side-app-backchannel-logout-guide.md`](/home/kyy/workspace/baron-sso/docs/server-side-app-backchannel-logout-guide.md) + +## 참고 구현 + +- [`backend/internal/service/backchannel_logout_service.go`](/home/kyy/workspace/baron-sso/backend/internal/service/backchannel_logout_service.go) +- [`backend/internal/handler/auth_handler.go`](/home/kyy/workspace/baron-sso/backend/internal/handler/auth_handler.go)