From 6c310c93b7d0fde3ec0405028f960bbdcc72f359 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Fri, 4 Sep 2026 14:56:58 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20C-2=20=E2=80=94=20nothing=20propagates?= =?UTF-8?q?=20because=20nobody=20implemented=20the=20receiving=20end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither client had a backchannel logout URL and the BFF has no oidcLogout configuration, so the three candidate paths all answer 302, which is the authentication redirect rather than a handler. Setting the URL on the identity provider alone changed nothing: with a live session, logging the user out emptied the Keycloak side and left the Redis session untouched. Reachability is not the blocker here, since a Keycloak pod fetches the app's public URL with a 200, but that is a property of this tailnet split-DNS lab and is the assumption most likely to fail in production, where it fails silently. Co-Authored-By: Claude Opus 5 --- .../page-2026-09-04T05-51-26-882Z.yml | 1 + .../page-2026-09-04T05-52-09-485Z.yml | 16 ++ .../page-2026-09-04T05-52-19-135Z.yml | 7 + .../page-2026-09-04T05-54-31-795Z.yml | 16 ++ .../page-2026-09-04T05-54-45-185Z.yml | 16 ++ .../page-2026-09-04T05-54-50-691Z.yml | 16 ++ .../page-2026-09-04T05-55-00-942Z.yml | 16 ++ .../page-2026-09-04T05-55-08-213Z.yml | 16 ++ .../page-2026-09-04T05-55-18-791Z.yml | 16 ++ .../01-current-state.txt | 12 + .../02-configure-idp.txt | 8 + .../03-logout-attempt.txt | 28 +++ .../c2-backchannel-logout/04-reachability.txt | 15 ++ docs/evidence/c2-backchannel-logout/README.md | 17 ++ docs/experiment-c2-backchannel-logout.md | 238 ++++++++++++++++++ 15 files changed, 438 insertions(+) create mode 100644 .playwright-mcp/page-2026-09-04T05-51-26-882Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-52-09-485Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-52-19-135Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-54-31-795Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-54-45-185Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-54-50-691Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-55-00-942Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-55-08-213Z.yml create mode 100644 .playwright-mcp/page-2026-09-04T05-55-18-791Z.yml create mode 100644 docs/evidence/c2-backchannel-logout/01-current-state.txt create mode 100644 docs/evidence/c2-backchannel-logout/02-configure-idp.txt create mode 100644 docs/evidence/c2-backchannel-logout/03-logout-attempt.txt create mode 100644 docs/evidence/c2-backchannel-logout/04-reachability.txt create mode 100644 docs/evidence/c2-backchannel-logout/README.md create mode 100644 docs/experiment-c2-backchannel-logout.md diff --git a/.playwright-mcp/page-2026-09-04T05-51-26-882Z.yml b/.playwright-mcp/page-2026-09-04T05-51-26-882Z.yml new file mode 100644 index 0000000..2e7c3f0 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-51-26-882Z.yml @@ -0,0 +1 @@ +- generic [active] [ref=f50e1]: "{\"pattern\":\"AP3-backend-for-frontend\",\"principal\":\"labuser\",\"accessTokenStoredOnServer\":true,\"refreshTokenStoredOnServer\":true,\"browserTokenCount\":0,\"csrfProtectionEnabled\":true}" \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-52-09-485Z.yml b/.playwright-mcp/page-2026-09-04T05-52-09-485Z.yml new file mode 100644 index 0000000..1b5c796 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-52-09-485Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f51e3]: + - banner [ref=f51e4]: + - generic [ref=f51e5]: keycloak-patterns + - main [ref=f51e6]: + - heading "Sign in to your account" [level=1] [ref=f51e8] + - generic [ref=f51e12]: + - generic [ref=f51e13]: + - generic [ref=f51e14]: Username or email + - textbox "Username or email" [active] [ref=f51e17] + - generic [ref=f51e18]: + - generic [ref=f51e19]: Password + - generic [ref=f51e21]: + - textbox "Password" [ref=f51e24] + - button "Show password" [ref=f51e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f51e27]:  + - button "Sign In" [ref=f51e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-52-19-135Z.yml b/.playwright-mcp/page-2026-09-04T05-52-19-135Z.yml new file mode 100644 index 0000000..714a618 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-52-19-135Z.yml @@ -0,0 +1,7 @@ +- main [ref=f52e2]: + - heading "AP3 · Backend-for-Frontend" [level=1] [ref=f52e3] + - paragraph [ref=f52e4]: 브라우저에는 OAuth token이 전혀 전달되지 않습니다. HttpOnly session cookie로 BFF만 호출하고, BFF가 서버 보관 access token을 Resource Server 요청에 붙입니다. + - button "Keycloak 로그인" [ref=f52e5] [cursor=pointer] + - button "token 경계 확인" [ref=f52e6] [cursor=pointer] + - button "BFF 경유 API 호출" [ref=f52e7] [cursor=pointer] + - button "CSRF token으로 상태 변경" [ref=f52e8] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-54-31-795Z.yml b/.playwright-mcp/page-2026-09-04T05-54-31-795Z.yml new file mode 100644 index 0000000..ea95873 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-54-31-795Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f53e3]: + - banner [ref=f53e4]: + - generic [ref=f53e5]: keycloak-patterns + - main [ref=f53e6]: + - heading "Sign in to your account" [level=1] [ref=f53e8] + - generic [ref=f53e12]: + - generic [ref=f53e13]: + - generic [ref=f53e14]: Username or email + - textbox "Username or email" [ref=f53e17] + - generic [ref=f53e18]: + - generic [ref=f53e19]: Password + - generic [ref=f53e21]: + - textbox "Password" [ref=f53e24] + - button "Show password" [ref=f53e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f53e27]:  + - button "Sign In" [ref=f53e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-54-45-185Z.yml b/.playwright-mcp/page-2026-09-04T05-54-45-185Z.yml new file mode 100644 index 0000000..79386bc --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-54-45-185Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f53e3]: + - banner [ref=f53e4]: + - generic [ref=f53e5]: keycloak-patterns + - main [ref=f53e6]: + - heading "Sign in to your account" [level=1] [ref=f53e8] + - generic [ref=f53e12]: + - generic [ref=f53e13]: + - generic [ref=f53e14]: Username or email + - textbox "Username or email" [ref=f53e17]: labuser + - generic [ref=f53e18]: + - generic [ref=f53e19]: Password + - generic [ref=f53e21]: + - textbox "Password" [active] [ref=f53e24]: labpass + - button "Show password" [ref=f53e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f53e27]:  + - button "Sign In" [ref=f53e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-54-50-691Z.yml b/.playwright-mcp/page-2026-09-04T05-54-50-691Z.yml new file mode 100644 index 0000000..79386bc --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-54-50-691Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f53e3]: + - banner [ref=f53e4]: + - generic [ref=f53e5]: keycloak-patterns + - main [ref=f53e6]: + - heading "Sign in to your account" [level=1] [ref=f53e8] + - generic [ref=f53e12]: + - generic [ref=f53e13]: + - generic [ref=f53e14]: Username or email + - textbox "Username or email" [ref=f53e17]: labuser + - generic [ref=f53e18]: + - generic [ref=f53e19]: Password + - generic [ref=f53e21]: + - textbox "Password" [active] [ref=f53e24]: labpass + - button "Show password" [ref=f53e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f53e27]:  + - button "Sign In" [ref=f53e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-55-00-942Z.yml b/.playwright-mcp/page-2026-09-04T05-55-00-942Z.yml new file mode 100644 index 0000000..79386bc --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-55-00-942Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f53e3]: + - banner [ref=f53e4]: + - generic [ref=f53e5]: keycloak-patterns + - main [ref=f53e6]: + - heading "Sign in to your account" [level=1] [ref=f53e8] + - generic [ref=f53e12]: + - generic [ref=f53e13]: + - generic [ref=f53e14]: Username or email + - textbox "Username or email" [ref=f53e17]: labuser + - generic [ref=f53e18]: + - generic [ref=f53e19]: Password + - generic [ref=f53e21]: + - textbox "Password" [active] [ref=f53e24]: labpass + - button "Show password" [ref=f53e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f53e27]:  + - button "Sign In" [ref=f53e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-55-08-213Z.yml b/.playwright-mcp/page-2026-09-04T05-55-08-213Z.yml new file mode 100644 index 0000000..8949eea --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-55-08-213Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f54e3]: + - banner [ref=f54e4]: + - generic [ref=f54e5]: keycloak-patterns + - main [ref=f54e6]: + - heading "Sign in to your account" [level=1] [ref=f54e8] + - generic [ref=f54e12]: + - generic [ref=f54e13]: + - generic [ref=f54e14]: Username or email + - textbox "Username or email" [ref=f54e17] + - generic [ref=f54e18]: + - generic [ref=f54e19]: Password + - generic [ref=f54e21]: + - textbox "Password" [ref=f54e24] + - button "Show password" [ref=f54e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f54e27]:  + - button "Sign In" [ref=f54e30] [cursor=pointer] \ No newline at end of file diff --git a/.playwright-mcp/page-2026-09-04T05-55-18-791Z.yml b/.playwright-mcp/page-2026-09-04T05-55-18-791Z.yml new file mode 100644 index 0000000..b87e097 --- /dev/null +++ b/.playwright-mcp/page-2026-09-04T05-55-18-791Z.yml @@ -0,0 +1,16 @@ +- generic [ref=f54e3]: + - banner [ref=f54e4]: + - generic [ref=f54e5]: keycloak-patterns + - main [ref=f54e6]: + - heading "Sign in to your account" [level=1] [ref=f54e8] + - generic [ref=f54e12]: + - generic [ref=f54e13]: + - generic [ref=f54e14]: Username or email + - textbox "Username or email" [ref=f54e17]: labuser + - generic [ref=f54e18]: + - generic [ref=f54e19]: Password + - generic [ref=f54e21]: + - textbox "Password" [active] [ref=f54e24]: labpass + - button "Show password" [ref=f54e26] [cursor=pointer]: + - generic [aria-hidden] [ref=f54e27]:  + - button "Sign In" [ref=f54e30] [cursor=pointer] \ No newline at end of file diff --git a/docs/evidence/c2-backchannel-logout/01-current-state.txt b/docs/evidence/c2-backchannel-logout/01-current-state.txt new file mode 100644 index 0000000..045fbc5 --- /dev/null +++ b/docs/evidence/c2-backchannel-logout/01-current-state.txt @@ -0,0 +1,12 @@ +=== 현재 클라이언트의 백채널 로그아웃 설정 === +--- bff-confidential --- + "frontchannelLogout" : false, +--- oauth2-proxy --- + "frontchannelLogout" : false, + +=== BFF 가 백채널 로그아웃 엔드포인트를 갖고 있는가 === + +=== 실제로 그 경로가 있는가 === + /logout/connect/back-channel/keycloak HTTP 302 + /backchannel-logout HTTP 302 + /oauth2/sign_out HTTP 302 diff --git a/docs/evidence/c2-backchannel-logout/02-configure-idp.txt b/docs/evidence/c2-backchannel-logout/02-configure-idp.txt new file mode 100644 index 0000000..5527211 --- /dev/null +++ b/docs/evidence/c2-backchannel-logout/02-configure-idp.txt @@ -0,0 +1,8 @@ +=== IdP 쪽에만 백채널 로그아웃 URL 을 설정한다 === + client id: 9055fa46-6abb-4d6d-a339-8a9183bbf26d +command terminated with exit code 1 + +=== 로그인 상태를 만든다 === + (브라우저에 이미 세션이 있다) + Keycloak 세션: 2 + Redis: 2 키 diff --git a/docs/evidence/c2-backchannel-logout/03-logout-attempt.txt b/docs/evidence/c2-backchannel-logout/03-logout-attempt.txt new file mode 100644 index 0000000..5648c75 --- /dev/null +++ b/docs/evidence/c2-backchannel-logout/03-logout-attempt.txt @@ -0,0 +1,28 @@ +=== 로그아웃 전 상태 === + Redis: 2 키 + keycloak-patterns 세션: 0 + +=== ★ IdP 로그아웃 — Keycloak 이 백채널 알림을 보낼 것이다 === + 시각: 14:53:29 + +=== Keycloak 로그 — 백채널 요청을 보냈는가, 결과는 === + +=== BFF 로그 — 백채널 요청이 도착했는가 === + +=== 앱 세션이 정리되었는가 === + Redis: 2 키 + _oauth2_proxy-6b028a70f69c8f0da9966eb36972dff2 + bff:session:sessions:6e0d9af4-2c8f-47d2-bf83-8b1e9670c679 +=== 로그아웃 전 — 실제 세션이 있는가 === + keycloak-patterns 세션: 1 + Redis: 1 키 + +=== ★ IdP 로그아웃 → 백채널 알림 === + 시각: 14:54:21 + +=== Keycloak 로그 === + +=== BFF 로그 — 요청이 왔는가 === + +=== 앱 세션 === + Redis: 1 키 diff --git a/docs/evidence/c2-backchannel-logout/04-reachability.txt b/docs/evidence/c2-backchannel-logout/04-reachability.txt new file mode 100644 index 0000000..db49316 --- /dev/null +++ b/docs/evidence/c2-backchannel-logout/04-reachability.txt @@ -0,0 +1,15 @@ +=== IdP 세션은 실제로 끊겼는가 === + keycloak-patterns 세션: 0 + +=== ★ Keycloak 파드가 app1.hyeonworks.com 에 닿는가 === + DNS 해석: + Address: 100.83.212.4 + + Non-authoritative answer: + + HTTPS 도달: + HTTP 200 (0 이면 못 닿음) + +=== Keycloak 로그 전체에서 backchannel 흔적 === + keycloak-0: 0 줄 + keycloak-1: 0 줄 diff --git a/docs/evidence/c2-backchannel-logout/README.md b/docs/evidence/c2-backchannel-logout/README.md new file mode 100644 index 0000000..8d28b1b --- /dev/null +++ b/docs/evidence/c2-backchannel-logout/README.md @@ -0,0 +1,17 @@ +# C-2 — 백채널 로그아웃 증거 + +2026-09-04 16:30–16:55 KST +해설: [`docs/experiment-c2-backchannel-logout.md`](../../experiment-c2-backchannel-logout.md) + +| 파일 | 무엇을 보여주는가 | +|---|---| +| `01-current-state.txt` | 두 클라이언트 모두 `backchannelLogoutUrl` 없음 · BFF 소스에 `oidcLogout` 없음 · 후보 경로 셋 다 **302**(핸들러 없음) | +| `02-configure-idp.txt` | IdP 쪽에만 `backchannel.logout.url` 설정 (점 표기는 실패, JSON 으로 성공) | +| `03-logout-attempt.txt` | **살아 있는 세션(1)에 로그아웃 → IdP 세션 0, Redis 세션은 1 그대로.** Keycloak·BFF 로그에 흔적 없음 | +| `04-reachability.txt` | **Keycloak 파드가 `app1.hyeonworks.com` 에 `HTTP 200` 으로 닿는다** — 네트워크 문제가 아님 | + +## 핵심 세 줄 + +1. **백채널 로그아웃은 어느 쪽에도 구현되어 있지 않았다.** C-1 이 관측한 "전파 안 됨"의 원인이다. +2. **IdP 쪽만 설정해도 소용없다.** 받을 엔드포인트와 `sid → 세션` 역인덱스가 앱에 있어야 한다. +3. **도달성이 숨은 전제다.** 이 실험대는 닿지만, 앱이 사설망에 있으면 설정해도 조용히 실패한다. diff --git a/docs/experiment-c2-backchannel-logout.md b/docs/experiment-c2-backchannel-logout.md new file mode 100644 index 0000000..6156b98 --- /dev/null +++ b/docs/experiment-c2-backchannel-logout.md @@ -0,0 +1,238 @@ +# C-2 — 로그아웃이 전 앱에 퍼지는가 (백채널 로그아웃) + +브랜치 `feature/keycloak-c2-backchannel-logout` · +증거 [`docs/evidence/c2-backchannel-logout/`](evidence/c2-backchannel-logout/) · +2026-09-04 16:30–16:55 KST + +선행: [`C-1`](experiment-c1-multi-app-sso.md) — **IdP 로그아웃이 앱에 전파되지 않는다**를 관측했다 + +--- + +## 0. 결론부터 + +| 확인 | 결과 | +|---|---| +| 백채널 로그아웃이 설정되어 있었는가 | **아니다.** 두 클라이언트 모두 `backchannelLogoutUrl` 없음 | +| BFF 에 그 엔드포인트가 있는가 | **아니다.** 소스에 `oidcLogout` 설정이 없다 | +| IdP 쪽만 설정하면 되는가 | **★ 안 된다.** 앱 세션이 그대로 남았다 | +| Keycloak 이 앱 URL 에 닿기는 하는가 | **닿는다** (`HTTP 200`) — 네트워크 문제가 아니다 | + +**C-1 이 관측한 "로그아웃이 안 퍼진다" 의 원인은 단순했다 — +아무도 구현하지 않았다.** + +--- + +## 1. 현재 상태 — 어느 쪽에도 없다 + +### IdP 쪽 + +```bash +kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes +``` + +``` + frontchannelLogout : false + (backchannel.logout.url 없음) +``` + +`oauth2-proxy` 클라이언트도 마찬가지였다. + +### 앱 쪽 + +```bash +grep -rn "oidcLogout\|backchannel" bff/src/main/java/ +``` + +**아무것도 안 나온다.** + +Spring Security 6.2+ 는 백채널 로그아웃을 지원하지만 **명시적으로 켜야 한다.** + +```java +.oidcLogout(oidc -> oidc.backChannel(Customizer.withDefaults())) +``` + +이 설정이 없으면 `/logout/connect/back-channel/{registrationId}` 경로가 +**생기지 않는다.** + +``` + /logout/connect/back-channel/keycloak HTTP 302 ← 로그인으로 리다이렉트 + /backchannel-logout HTTP 302 + /oauth2/sign_out HTTP 302 +``` + +**302 는 "그런 핸들러가 없어서 인증 요구로 떨어졌다"는 뜻**이다. +엔드포인트가 있었다면 POST 를 받아 200 이나 400 을 돌려줬을 것이다. + +--- + +## 2. IdP 쪽만 설정하고 시험했다 + +```bash +kcadm.sh update clients/ -r keycloak-patterns \ + -s 'attributes={"backchannel.logout.url":"https://app1.hyeonworks.com/logout/connect/back-channel/keycloak", + "backchannel.logout.session.required":"true"}' +``` + +``` + backchannel.logout.session.required = true + backchannel.logout.url = https://app1.hyeonworks.com/logout/connect/back-channel/keycloak +``` + +### 살아 있는 세션에 로그아웃을 걸었다 + +``` +=== 로그아웃 전 === + keycloak-patterns 세션: 1 + Redis: 1 키 + +=== IdP 로그아웃 === + kcadm.sh create users//logout -r keycloak-patterns + +=== 결과 === + keycloak-patterns 세션: 0 ← IdP 쪽은 끊겼다 + Redis: 1 키 ← ★ 앱 세션은 그대로다 +``` + +**IdP 세션만 사라지고 앱 세션은 남았다.** C-1 과 같은 결과다. + +### 네트워크 문제가 아님을 확인했다 + +``` +=== Keycloak 파드가 app1.hyeonworks.com 에 닿는가 === + DNS 해석: Address: 100.83.212.4 + HTTPS 도달: HTTP 200 +``` + +**클러스터 안에서 공개 이름으로 앱에 닿는다.** 이 실험대는 tailnet + split DNS +구성이라 헤어핀이 되는데, **운영에서는 안 되는 경우가 흔하다.** + +> **백채널 로그아웃의 숨은 전제** — IdP 가 **앱의 공개 URL 로 서버에서 서버로** +> 요청을 보낼 수 있어야 한다. 앱이 사설망에 있고 IdP 가 밖에 있으면 +> **설정을 해도 도달하지 못한다.** 그때는 로그도 안 남고 조용히 실패한다. + +--- + +## 3. 그래서 왜 안 퍼졌는가 + +``` + IdP 로그아웃 + ├─ ① Keycloak 이 backchannel.logout.url 로 POST 를 보낸다 (설정함) + ├─ ② 앱이 그 POST 를 받는 엔드포인트를 갖고 있다 ★ 없다 + └─ ③ 앱이 logout token 을 검증하고 sid 로 세션을 찾아 지운다 ★ 없다 +``` + +**②와 ③이 없다.** ①만 설정해도 받을 사람이 없다. + +Keycloak 로그에 `backchannel` 문자열이 **0줄**이었다 — 다만 이것만으로 +"보내지 않았다"고 단정할 수는 없다 (DEBUG 레벨일 수 있다). +**확실한 것은 앱 세션이 남았다는 관측**이다. + +--- + +## 4. 개념 — 백채널 로그아웃의 구조 + +``` + 사용자가 어느 앱에서든 로그아웃 + │ + ▼ + Keycloak 이 SSO 세션에 붙은 client session 목록을 본다 (C-1 의 그 구조) + │ + ├──POST──▶ app1 의 backchannel.logout.url + └──POST──▶ app2 의 backchannel.logout.url + 본문: logout_token (JWT) + { "sid": "...", "sub": "...", "events": {...} } +``` + +### `sid` 가 여기서 쓰인다 + +**A-0 에서 확인한 그 `sid`** 다 — JWT·DB·관리 API 에서 같은 문자열이었던. + +``` + logout_token 의 sid → 앱이 "그 sid 로 만든 내 세션"을 찾아 지운다 +``` + +**그래서 앱은 `sid → 자기 세션 ID` 역인덱스를 갖고 있어야 한다.** +Spring Security 는 이를 위해 `OidcSessionRegistry` 를 쓰며, +**여러 인스턴스가 있으면 그 레지스트리도 공유 저장소여야 한다** — +B-1·B-2 에서 겪은 것과 **같은 문제가 한 겹 더 있다.** + +### 부분 실패는 어떻게 되는가 + +``` + app1 로그아웃 성공, app2 는 응답 없음 + └─ Keycloak 은 재시도하는가? 얼마나? + └─ 사용자는 app2 에서 여전히 로그인 상태다 +``` + +**로그아웃은 원자적이지 않다.** 앱이 늘어날수록 "일부만 로그아웃된 상태"가 +생길 확률이 올라간다. + +--- + +## 5. 겪은 문제 + +| 문제 | | +|---|---| +| `kcadm -s "attributes.backchannel.logout.url=..."` 이 exit 1 | 점 표기가 안 먹는다. **JSON 으로 통째로** 줘야 한다 | +| 첫 시험이 무의미했다 | 로그아웃 전 IdP 세션이 **이미 0** 이었다. 끊을 대상이 없었다 | +| 세션 수를 realm 없이 셌다 | C-1 과 같은 실수 — `master` 의 admin 세션이 섞인다 | +| Keycloak 재시작 후 로그인 폼이 안 넘어갔다 | 인증 세션 쿠키가 무효화된 상태에서 폼을 재사용했다 | + +**두 번째가 A층에서 반복한 교훈이다** — **주입 대상이 실제로 존재하는지 +먼저 확인한다.** 세션이 없는 상태에서 로그아웃을 걸고 "전파가 안 된다"고 +결론지을 뻔했다. + +--- + +## 6. 구현하려면 무엇이 필요한가 + +| 계층 | 할 일 | +|---|---| +| **IdP** | 클라이언트마다 `backchannel.logout.url` 설정 (완료) | +| **앱** | `.oidcLogout(oidc -> oidc.backChannel(...))` 활성화 | +| **앱** | `OidcSessionRegistry` 를 **공유 저장소**로 (인스턴스가 여럿이므로) | +| **네트워크** | IdP → 앱 공개 URL 도달 (이 실험대는 됨, 운영은 확인 필요) | +| **oauth2-proxy** | **지원하지 않는다.** 별도 방안이 필요하다 | + +**마지막이 C-1 과 맞물린다** — app1(BFF)은 구현할 수 있지만 +app2(oauth2-proxy)는 못 한다. **한 SSO 안에서 로그아웃 전파가 앱마다 +다르게 동작하게 된다.** + +--- + +## 7. 재현 절차 (명령어) + +```bash +# 1. 현재 설정 확인 — 어느 쪽에도 없다 +kcadm.sh get clients -r keycloak-patterns -q clientId=bff-confidential --fields attributes +grep -rn "oidcLogout\|backchannel" bff/src/main/java/ +curl -s -o /dev/null -w '%{http_code}\n' -X POST https://app1.hyeonworks.com/logout/connect/back-channel/keycloak + +# 2. IdP 쪽 설정 — 점 표기는 안 먹는다. JSON 으로 +kcadm.sh update clients/ -r keycloak-patterns \ + -s 'attributes={"backchannel.logout.url":"...","backchannel.logout.session.required":"true"}' + +# 3. ★ 살아 있는 세션이 있는지 먼저 확인한다 (realm 을 join 해서) +kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \ + "select count(*) from offline_user_session us join realm r on r.id=us.realm_id + where r.name='keycloak-patterns' and us.offline_flag='0'" + +# 4. 로그아웃하고 앱 세션을 본다 +kcadm.sh create users//logout -r keycloak-patterns +kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize + +# 5. 도달성 확인 — 클러스터 안에서 앱 공개 URL 로 +kubectl -n keycloak-lab run t --rm -i --restart=Never --image=curlimages/curl:8.11.1 \ + --command -- curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/ +``` + +--- + +## 8. 다음에 남기는 것 + +| | | +|---|---| +| **구현** | `.oidcLogout()` 활성화 + `OidcSessionRegistry` 공유 | +| **oauth2-proxy** | 백채널 로그아웃 미지원 — SSO 안에서 앱마다 동작이 갈린다 | +| **운영** | IdP → 앱 도달성이 전제다. 안 되면 **조용히 실패한다** | +| **B-2 와 연결** | 로그아웃이 지우는 것은 지금도 세 곳 중 하나뿐이다 |