1
0
forked from baron/baron-sso
Files
baron-sso/docs/pkce-backchannel-logout-guide.md

9.9 KiB

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

예시:

Type: pkce
Redirect URI: https://rp.example.com/callback
Back-Channel Logout URI: https://rp.example.com/backchannel-logout
SID Claim Required: off

로컬 Docker 개발 예시:

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 구조를 가정

예시:

sid: 796f5cf7-37e7-494b-9b4c-26cc0c217a6a
sub: 8150cb83-a905-4b50-bdcf-d22046ecdc30
rpSessionId: DqKlQ8MbsGnn_jfOus1k03MFRDpuXCrj

2. POST /backchannel-logout endpoint

RP는 Baron이 서버 간으로 호출할 endpoint를 제공해야 합니다.

예:

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 예시는 다음과 같습니다.

GET /api/v1/auth/backchannel/jwks.json

검증 필수 항목:

  1. JWT 서명 검증
  2. iss가 Baron OIDC issuer와 일치
  3. aud에 현재 RP client_id 포함
  4. iat 존재
  5. jti 존재
  6. eventshttp://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_tokensid가 있어야만 처리
  • sub fallback 금지
  • 세션 모델이 sid 중심으로 안정적으로 유지되는 RP에 적합

SID Claim Required = false

  • sid가 있으면 우선 사용
  • sid 매칭이 안 되거나 sid가 없어도 sub로 fallback 가능
  • 실제 운영에서는 이 모드가 더 현실적일 수 있음

세션 파기 방식

Back-Channel Logout에서는 현재 브라우저 요청의 req.session.destroy()로는 부족합니다.
반드시 세션 스토어에서 session id를 찾아 직접 파기해야 합니다.

예:

store.destroy(rpSessionId)

필수 조건:

  • 로그아웃 대상 세션 ID를 매핑 테이블에서 찾을 수 있어야 함
  • 이미 삭제된 세션은 idempotent success 처리

권장 로그 항목

RP는 아래 정도의 로그를 남기는 것을 권장합니다.

  1. 요청 수신
  2. 토큰 검증 성공/실패
  3. sid, sub, jti
  4. 매칭된 rpSessionId 목록
  5. 세션 파기 성공/실패 수

예시:

[백채널 로그아웃] 요청 수신
[백채널 로그아웃] 토큰 검증 성공
[백채널 로그아웃] 세션 탐색 결과
[백채널 로그아웃] 세션 파기 완료
[백채널 로그아웃] 처리 완료

주의:

  • 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 기준 최소 구조 예시는 다음과 같습니다.

GET  /login
GET  /callback
GET  /profile
GET  /logout
POST /backchannel-logout

내부 저장 예시:

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가 실제로 그 주소에 도달하는 것은 다릅니다.

예:

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 수신용 서버 컴포넌트를 추가해야 합니다.

로직 흐름

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. sidsub를 모두 저장
  4. 세션 스토어에서 직접 세션 파기
  5. 로컬 개발 시 Baron backend가 도달 가능한 URI를 사용

이 다섯 가지가 갖춰져야 Baron의 원격 세션 종료가 RP 로컬 세션 종료까지 이어집니다.