forked from baron/baron-sso
PCKE 구현 가이드 문서 추가
This commit is contained in:
321
docs/pkce-backchannel-logout-guide.md
Normal file
321
docs/pkce-backchannel-logout-guide.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# PKCE RP Back-Channel Logout 구현 가이드
|
||||
|
||||
이 문서는 Baron SSO와 연동하는 PKCE RP가 `Back-Channel Logout`을 지원하려고 할 때 필요한 구현 기준을 정리합니다.
|
||||
|
||||
## 목적
|
||||
|
||||
PKCE RP도 OIDC `Authorization Code + PKCE` 흐름을 사용하면서 Baron SSO의 원격 세션 종료 이벤트를 받을 수 있어야 합니다. 다만 `Back-Channel Logout`은 브라우저가 아니라 OP(Baron)가 RP 서버로 직접 `logout_token`을 보내는 방식이므로, **순수 frontend-only PKCE 앱만으로는 구현할 수 없습니다.**
|
||||
|
||||
즉, PKCE RP가 `Back-Channel Logout`을 사용하려면 다음 둘을 모두 가져야 합니다.
|
||||
|
||||
1. PKCE 로그인 플로우를 시작하고 callback을 처리하는 RP
|
||||
2. `logout_token`을 수신하는 서버 endpoint
|
||||
|
||||
## 적용 대상
|
||||
|
||||
이 가이드는 다음 경우를 대상으로 합니다.
|
||||
|
||||
- 브라우저에서 `Authorization Code + PKCE`를 사용하는 RP
|
||||
- RP가 자체 세션 또는 BFF 세션을 보유하는 경우
|
||||
- RP가 `Back-Channel Logout URI`를 등록하고 Baron의 세션 종료 이벤트를 직접 수신하려는 경우
|
||||
|
||||
다음 경우는 이 가이드의 직접 대상이 아닙니다.
|
||||
|
||||
- 순수 frontend-only SPA
|
||||
- 서버 없이 `localStorage`/`sessionStorage`만 사용하는 PKCE 앱
|
||||
|
||||
이 경우에는 `Back-Channel Logout` 대신 front-channel logout, 세션 재검증, 짧은 token TTL 같은 별도 전략을 사용해야 합니다.
|
||||
|
||||
## devfront 등록 기준
|
||||
|
||||
PKCE RP는 devfront에서 아래 항목을 등록합니다.
|
||||
|
||||
1. `Type`: `pkce`
|
||||
2. `Redirect URI`: RP callback URL
|
||||
3. `Back-Channel Logout URI`: RP 서버 endpoint
|
||||
4. 필요 시 `SID Claim Required`
|
||||
|
||||
예시:
|
||||
|
||||
```text
|
||||
Type: pkce
|
||||
Redirect URI: https://rp.example.com/callback
|
||||
Back-Channel Logout URI: https://rp.example.com/backchannel-logout
|
||||
SID Claim Required: off
|
||||
```
|
||||
|
||||
로컬 Docker 개발 예시:
|
||||
|
||||
```text
|
||||
Redirect URI: http://localhost:3333/callback
|
||||
Back-Channel Logout URI: http://baron-sso-login-demo:3333/backchannel-logout
|
||||
```
|
||||
|
||||
주의:
|
||||
|
||||
- `Back-Channel Logout URI`는 **브라우저 기준 주소가 아니라 Baron backend가 실제로 접근 가능한 주소**여야 합니다.
|
||||
- Docker 환경에서는 `localhost`가 backend 컨테이너 자신을 가리킬 수 있으므로, Docker 서비스명이나 사설 IP를 사용해야 할 수 있습니다.
|
||||
|
||||
## 구현 요구사항
|
||||
|
||||
PKCE RP는 최소한 아래를 구현해야 합니다.
|
||||
|
||||
### 1. 로그인 후 세션 매핑 저장
|
||||
|
||||
RP는 callback 이후 아래 정보 중 하나 이상을 로컬 세션과 연결해야 합니다.
|
||||
|
||||
- `sid -> rpSessionId`
|
||||
- `sub -> rpSessionId`
|
||||
|
||||
권장 순서는 다음과 같습니다.
|
||||
|
||||
1. `sid`를 우선 저장
|
||||
2. `sub`도 함께 저장
|
||||
3. 한 사용자가 여러 브라우저 세션을 가질 수 있으므로 `1:N` 구조를 가정
|
||||
|
||||
예시:
|
||||
|
||||
```text
|
||||
sid: 796f5cf7-37e7-494b-9b4c-26cc0c217a6a
|
||||
sub: 8150cb83-a905-4b50-bdcf-d22046ecdc30
|
||||
rpSessionId: DqKlQ8MbsGnn_jfOus1k03MFRDpuXCrj
|
||||
```
|
||||
|
||||
### 2. `POST /backchannel-logout` endpoint
|
||||
|
||||
RP는 Baron이 서버 간으로 호출할 endpoint를 제공해야 합니다.
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
POST /backchannel-logout
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Body: logout_token=<jwt>
|
||||
```
|
||||
|
||||
RP는 이 endpoint에서:
|
||||
|
||||
1. `logout_token` 존재 여부 확인
|
||||
2. JWT 서명 및 claim 검증
|
||||
3. `sid` 또는 `sub`로 로컬 세션 탐색
|
||||
4. 세션 스토어에서 직접 세션 파기
|
||||
5. 성공 시 `2xx` 응답
|
||||
|
||||
을 수행해야 합니다.
|
||||
|
||||
### 3. `logout_token` 검증
|
||||
|
||||
RP는 Baron이 노출하는 Back-Channel Logout JWKS로 `logout_token`을 검증해야 합니다.
|
||||
|
||||
현재 Baron의 JWKS endpoint 예시는 다음과 같습니다.
|
||||
|
||||
```text
|
||||
GET /api/v1/auth/backchannel/jwks.json
|
||||
```
|
||||
|
||||
검증 필수 항목:
|
||||
|
||||
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`
|
||||
|
||||
## 세션 종료 기준
|
||||
|
||||
### 권장 순서
|
||||
|
||||
1. `sid`로 매칭 시도
|
||||
2. 매칭 실패 시 `sub`로 fallback
|
||||
|
||||
이 기준은 `SID Claim Required` 정책에 따라 달라집니다.
|
||||
|
||||
### `SID Claim Required = true`
|
||||
|
||||
- `logout_token`에 `sid`가 있어야만 처리
|
||||
- `sub` fallback 금지
|
||||
- 세션 모델이 `sid` 중심으로 안정적으로 유지되는 RP에 적합
|
||||
|
||||
### `SID Claim Required = false`
|
||||
|
||||
- `sid`가 있으면 우선 사용
|
||||
- `sid` 매칭이 안 되거나 `sid`가 없어도 `sub`로 fallback 가능
|
||||
- 실제 운영에서는 이 모드가 더 현실적일 수 있음
|
||||
|
||||
## 세션 파기 방식
|
||||
|
||||
`Back-Channel Logout`에서는 현재 브라우저 요청의 `req.session.destroy()`로는 부족합니다.
|
||||
반드시 **세션 스토어에서 session id를 찾아 직접 파기**해야 합니다.
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
store.destroy(rpSessionId)
|
||||
```
|
||||
|
||||
필수 조건:
|
||||
|
||||
- 로그아웃 대상 세션 ID를 매핑 테이블에서 찾을 수 있어야 함
|
||||
- 이미 삭제된 세션은 idempotent success 처리
|
||||
|
||||
## 권장 로그 항목
|
||||
|
||||
RP는 아래 정도의 로그를 남기는 것을 권장합니다.
|
||||
|
||||
1. 요청 수신
|
||||
2. 토큰 검증 성공/실패
|
||||
3. `sid`, `sub`, `jti`
|
||||
4. 매칭된 `rpSessionId` 목록
|
||||
5. 세션 파기 성공/실패 수
|
||||
|
||||
예시:
|
||||
|
||||
```text
|
||||
[백채널 로그아웃] 요청 수신
|
||||
[백채널 로그아웃] 토큰 검증 성공
|
||||
[백채널 로그아웃] 세션 탐색 결과
|
||||
[백채널 로그아웃] 세션 파기 완료
|
||||
[백채널 로그아웃] 처리 완료
|
||||
```
|
||||
|
||||
주의:
|
||||
|
||||
- raw `logout_token` 전체를 로그에 남기지 않습니다.
|
||||
- access token, refresh token, cookie raw value도 남기지 않습니다.
|
||||
|
||||
## 테스트 체크리스트
|
||||
|
||||
### 기본 성공 시나리오
|
||||
|
||||
1. PKCE RP 로그인
|
||||
2. callback 후 `sid/sub -> rpSessionId` 매핑 생성 확인
|
||||
3. UserFront에서 `세션 종료`
|
||||
4. Baron이 RP의 `Back-Channel Logout URI`로 POST
|
||||
5. RP가 `logout_token` 검증 성공
|
||||
6. RP 세션 파기 성공
|
||||
7. 보호 페이지 접근 시 비로그인 상태 확인
|
||||
|
||||
### 확인 포인트
|
||||
|
||||
1. devfront에 `Back-Channel Logout URI`가 실제 저장됐는가
|
||||
2. Baron backend가 해당 URI에 실제로 도달 가능한가
|
||||
3. RP 로그에 `요청 수신`과 `토큰 검증 성공`이 찍히는가
|
||||
4. 세션 스토어에서 실제 세션이 삭제됐는가
|
||||
5. `SID Claim Required=true`일 때와 `false`일 때 결과가 의도대로 다른가
|
||||
|
||||
## 구현 예시 구조
|
||||
|
||||
Node.js/Express 기준 최소 구조 예시는 다음과 같습니다.
|
||||
|
||||
```text
|
||||
GET /login
|
||||
GET /callback
|
||||
GET /profile
|
||||
GET /logout
|
||||
POST /backchannel-logout
|
||||
```
|
||||
|
||||
내부 저장 예시:
|
||||
|
||||
```text
|
||||
sidToSessionIds: Map<string, Set<string>>
|
||||
subToSessionIds: Map<string, Set<string>>
|
||||
sessionIdToBinding: Map<string, { sid: string, sub: string }>
|
||||
```
|
||||
|
||||
실제 분리 예시는 아래 데모 코드를 참고할 수 있습니다.
|
||||
|
||||
- 백채널 로그아웃 모듈: `https://gitea.hmac.kr/kyy/pkce-login-demo/src/branch/main/backchannel-logout.js`
|
||||
- 데모 앱 엔트리포인트: `https://gitea.hmac.kr/kyy/pkce-login-demo/src/branch/main/app.js`
|
||||
|
||||
이 데모는:
|
||||
|
||||
1. callback 이후 `registerSessionBinding()`으로 `sid/sub -> sessionId`를 등록
|
||||
2. `POST /backchannel-logout`에서 `handleBackchannelLogout`를 그대로 연결
|
||||
3. 로컬 `/logout` 또는 세션 정리 시 `removeSessionBinding()` 호출
|
||||
|
||||
구조로 동작합니다.
|
||||
|
||||
## 자주 생기는 문제
|
||||
|
||||
### 1. `localhost`로는 안 되는데 입력은 저장됨
|
||||
|
||||
입력 validation을 통과하는 것과 Baron backend가 실제로 그 주소에 도달하는 것은 다릅니다.
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
http://localhost:3333/backchannel-logout
|
||||
```
|
||||
|
||||
이 값은 backend 컨테이너 기준으로는 자기 자신을 가리킬 수 있습니다. Docker 환경에서는 Docker 서비스명 또는 사설 IP를 사용해야 할 수 있습니다.
|
||||
|
||||
### 2. `sid`가 로그인 시 값과 다름
|
||||
|
||||
실제 운영에서는 `logout_token.sid`가 RP가 저장한 `sid`와 항상 같다고 가정하면 안 됩니다.
|
||||
|
||||
따라서:
|
||||
|
||||
1. `sid` 우선
|
||||
2. `sub` fallback
|
||||
|
||||
구현을 권장합니다. 다만 보안 정책상 `SID Claim Required=true`를 선택한 경우에는 fallback 없이 `sid`만 사용해야 합니다.
|
||||
|
||||
### 3. 순수 frontend-only PKCE인데 endpoint를 만들 수 없음
|
||||
|
||||
그 경우는 `Back-Channel Logout` 자체를 구현할 수 없습니다. 최소한 logout 수신용 서버 컴포넌트를 추가해야 합니다.
|
||||
|
||||
## 로직 흐름
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Browser as 브라우저
|
||||
participant RP as PKCE RP
|
||||
participant Baron as Baron SSO
|
||||
participant Store as 세션 스토어
|
||||
|
||||
Browser->>RP: GET /login 호출
|
||||
RP->>Browser: Baron authorize endpoint로 리다이렉트
|
||||
Browser->>Baron: Authorization Code + PKCE 로그인
|
||||
Baron->>Browser: /callback?code=... 으로 리다이렉트
|
||||
Browser->>RP: GET /callback 호출
|
||||
RP->>Baron: code_verifier 포함 token 요청
|
||||
Baron-->>RP: ID Token / Access Token 반환
|
||||
RP->>Store: RP 세션 생성
|
||||
RP->>RP: registerSessionBinding(sessionId, sid, sub)
|
||||
RP-->>Browser: 로그인 완료 응답
|
||||
|
||||
Browser->>Baron: UserFront 또는 연동 서비스에서 세션 종료
|
||||
Baron->>RP: POST /backchannel-logout (logout_token)
|
||||
RP->>Baron: Back-Channel JWKS로 logout_token 검증
|
||||
Baron-->>RP: 서명 / issuer / audience 검증 기준 제공
|
||||
RP->>RP: sid 또는 sub로 sessionId 탐색
|
||||
RP->>Store: destroy(sessionId)
|
||||
RP->>RP: removeSessionBinding(sessionId)
|
||||
RP-->>Baron: 200 OK
|
||||
|
||||
Browser->>RP: GET /profile 호출
|
||||
RP-->>Browser: 루트 리다이렉트 또는 비로그인 응답
|
||||
```
|
||||
|
||||
## 권장 결론
|
||||
|
||||
PKCE RP에서 `Back-Channel Logout`을 쓰려면, 다음 원칙을 따르십시오.
|
||||
|
||||
1. PKCE 로그인 플로우는 그대로 유지
|
||||
2. logout 수신용 서버 endpoint 별도 구현
|
||||
3. `sid`와 `sub`를 모두 저장
|
||||
4. 세션 스토어에서 직접 세션 파기
|
||||
5. 로컬 개발 시 Baron backend가 도달 가능한 URI를 사용
|
||||
|
||||
이 다섯 가지가 갖춰져야 Baron의 원격 세션 종료가 RP 로컬 세션 종료까지 이어집니다.
|
||||
Reference in New Issue
Block a user