Skip to content

필사 모드: Single Logout(SLO)의 어려움 — Front-Channel, Back-Channel 로그아웃 설계

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — 로그인보다 로그아웃이 어렵다

SSO 프로젝트에서 가장 늦게 발견되고 가장 오래 끄는 이슈는 의외로 로그아웃입니다. 로그인은 "한 곳에서 인증하면 모두 열린다"는 단방향 흐름이라 설계가 명쾌합니다. 그러나 로그아웃은 "한 곳에서 끊으면 모두 닫혀야 한다"는 **분산 상태 전파 문제**입니다. 분산 시스템에서 상태 전파가 얼마나 어려운지 아는 엔지니어라면, Single Logout(SLO)이 왜 수많은 표준(OIDC만 세 가지 로그아웃 스펙)을 낳고도 여전히 말끔하지 않은지 짐작할 수 있을 것입니다.

2026년 현재 이 문제는 더 어려워졌습니다. 모든 주류 브라우저가 서드파티 쿠키를 기본 차단하면서, 십수 년간 SLO의 주력이던 front-channel(iframe) 방식이 사실상 수명을 다했기 때문입니다. 이 글에서는 SSO 세션의 3계층 모델부터 OIDC의 세 가지 로그아웃 스펙, SAML SLO의 현실, Keycloak 설정, 모바일·UX·테스트까지 로그아웃 설계의 전체 지도를 그립니다.

SSO 세션의 3계층 모델 — 무엇을 끊어야 하는가

로그아웃을 설계하려면 먼저 "로그인 상태"가 어디에 몇 개나 존재하는지 알아야 합니다. 전형적인 OIDC SSO 환경에는 최소 3계층의 세션이 존재합니다.

계층 1: IdP(OP) SSO 세션

- Keycloak 등 IdP의 브라우저 쿠키 (예: KEYCLOAK_IDENTITY)

- 이것이 살아 있으면 어느 앱이든 재인증 없이 SSO 로그인 가능

계층 2: 각 앱(RP)의 자체 세션

- 앱 서버 세션 쿠키 (예: JSESSIONID) 또는 앱 자체 세션 저장소

- 앱은 한 번 OIDC 인증을 받은 뒤 자기 세션으로 사용자를 기억

계층 3: 발급된 토큰들

- access token (자체 수명 동안 유효 — 세션과 무관하게 동작 가능)

- refresh token (회수 전까지 새 access token 발급 가능)

- 모바일 앱/SPA가 들고 있는 토큰 사본

도식으로 보면 이렇습니다.

+---------------------------+

| IdP (Keycloak) |

| [계층1] SSO 세션 쿠키 |

+------+--------+-----------+

| |

OIDC 인증 | | OIDC 인증

v v

+--------------+--+ +--+---------------+

| 앱 A (RP) | | 앱 B (RP) |

| [계층2] 앱 세션 | | [계층2] 앱 세션 |

| [계층3] 토큰들 | | [계층3] 토큰들 |

+-----------------+ +------------------+

"로그아웃했다"가 의미를 가지려면 **세 계층이 모두 정리**되어야 합니다. 흔한 실패 모드는 다음과 같습니다.

| 실패 모드 | 증상 | 원인 |

| --- | --- | --- |

| 앱 세션만 종료 | 앱 A에서 로그아웃 직후 다시 로그인하면 비밀번호 없이 들어옴 | IdP SSO 세션(계층1)이 살아 있음 |

| IdP 세션만 종료 | IdP에서 로그아웃해도 앱 B는 계속 사용 가능 | 앱 B의 자체 세션(계층2)이 살아 있음 |

| 세션은 다 끊었는데 API는 동작 | 로그아웃 후에도 모바일 앱이 API 호출 성공 | access/refresh token(계층3)이 회수되지 않음 |

이 표의 두 번째 줄이 바로 SLO가 풀려는 문제입니다. IdP에서 세션이 끊겼다는 사실을 **모든 RP에게 전파**해야 합니다.

OIDC RP-Initiated Logout — 로그아웃의 시작점

사용자가 앱 A에서 "로그아웃"을 누르면, 앱 A는 자기 세션만 끊는 게 아니라 IdP의 로그아웃 엔드포인트로 사용자를 보냅니다. 이것이 [OIDC RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)입니다.

GET /realms/saas-prod/protocol/openid-connect/logout?id_token_hint=eyJhbGciOiJSUzI1NiIs...&post_logout_redirect_uri=https%3A%2F%2Fapp-a.example.com%2Fbye&state=af0ifjsldkj HTTP/1.1

Host: auth.example.com

파라미터의 의미입니다.

- **id_token_hint** — 이전에 발급받은 ID 토큰. 누구의 어떤 세션을 끊는지 IdP가 확인하는 근거입니다. 이것이 없으면 IdP는 "정말 로그아웃하시겠습니까?" 확인 화면을 띄우는 것이 보통입니다(로그아웃 CSRF 방지).

- **post_logout_redirect_uri** — 로그아웃 완료 후 돌아갈 곳. 사전 등록된 URI만 허용해야 합니다(오픈 리다이렉트 방지).

- **state** — 리다이렉트 왕복 간 CSRF 방지용 불투명 값.

여기까지는 "앱 A와 IdP" 사이의 정리일 뿐입니다. 앱 B, C, D에게 알리는 것이 진짜 문제이고, 그 방법이 front-channel과 back-channel입니다.

Front-Channel Logout — iframe의 영광과 몰락

[OIDC Front-Channel Logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html)은 브라우저를 전파 매체로 씁니다. IdP의 로그아웃 페이지가 각 RP의 로그아웃 URL을 숨겨진 iframe으로 렌더링하면, 각 iframe 요청에 RP의 세션 쿠키가 실려가 RP가 자기 세션을 끊는 방식입니다.

[브라우저가 IdP 로그아웃 페이지 로드]

IdP 응답 HTML 안에:

iframe 1 -> https://app-a.example.com/oidc/front-logout?iss=...&sid=abc123

iframe 2 -> https://app-b.example.com/oidc/front-logout?iss=...&sid=abc123

iframe 3 -> https://app-c.example.com/oidc/front-logout?iss=...&sid=abc123

각 RP는 요청에 실려온 "자기 도메인 쿠키"로 세션을 찾아 종료

이 방식의 결정적 약점이 2026년에 치명상이 되었습니다.

1. **서드파티 쿠키 차단**: iframe 안의 app-a.example.com 요청은 IdP 페이지(auth.example.com) 맥락에서 보면 서드파티 요청입니다. Safari(ITP)는 오래전부터, Chrome도 단계적 차단을 거쳐, 2026년 현재 주류 브라우저 기본 설정에서 이 iframe 요청에는 **RP의 세션 쿠키가 실리지 않습니다**. 쿠키가 없으면 RP는 어느 세션을 끊어야 할지 모릅니다. 즉 front-channel logout은 조용히 실패합니다.

2. **결과 확인 불가**: iframe 로드가 성공했는지 IdP가 알 수 없습니다. 실패해도 재시도가 없습니다.

3. **타이밍**: 사용자가 로그아웃 페이지를 즉시 떠나면 iframe 로드가 끝나기 전에 중단될 수 있습니다.

같은 도메인 패밀리(예: IdP와 앱이 모두 example.com의 서브도메인)라면 퍼스트파티로 간주되어 아직 동작하지만, 멀티테넌트 SaaS처럼 도메인이 갈라지는 순간 front-channel은 신뢰할 수 없습니다. **2026년의 결론: front-channel logout은 신규 설계에서 선택하지 않습니다.**

Back-Channel Logout — 서버 간 직접 통보

[OIDC Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html)은 브라우저를 우회합니다. IdP가 각 RP의 백엔드 엔드포인트로 **logout token**이라는 서명된 JWT를 직접 POST합니다.

[IdP 세션 종료 발생]

|

| HTTP POST (서버 -> 서버, 브라우저 무관)

+---> https://app-a.example.com/oidc/back-logout (logout_token=...)

+---> https://app-b.example.com/oidc/back-logout (logout_token=...)

+---> https://app-c.example.com/oidc/back-logout (logout_token=...)

Logout Token의 구조

logout token은 ID 토큰과 유사한 JWT지만 몇 가지 고유 규칙이 있습니다.

{

"iss": "https://auth.example.com/realms/saas-prod",

"aud": "app-a-client",

"iat": 1781234567,

"exp": 1781234687,

"jti": "bWJq-09cd-4a4f-a3f9",

"sub": "f3a8c2e1-9b47-4d6a-8c21-0e5f7a9b3d44",

"sid": "abc123-session-id",

"events": {

"http://schemas.openid.net/event/backchannel-logout": {}

}

}

검증 규칙이 중요합니다.

- **events 클레임 필수**: backchannel-logout 이벤트 URI가 키로 존재해야 합니다.

- **nonce 금지**: ID 토큰과의 혼동(토큰 치환 공격)을 막기 위해 nonce가 있으면 거부해야 합니다.

- **sub 또는 sid 중 최소 하나**: sid가 있으면 "그 세션만", sub만 있으면 "그 사용자의 모든 세션"을 끊으라는 의미입니다.

- **서명 검증**: IdP의 JWKS로 서명을 확인하고 iss/aud/exp를 검증합니다.

- **jti 재사용 감지**: 리플레이 방지를 위해 처리한 jti를 짧은 기간 기억합니다.

RP 쪽 수신 엔드포인트 구현

@PostMapping(value = "/oidc/back-logout",

consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)

public ResponseEntity<Void> backChannelLogout(@RequestParam("logout_token") String token) {

LogoutToken lt = logoutTokenValidator.validate(token); // 서명, iss, aud, events, nonce 부재 검증

if (lt.sid() != null) {

sessionRegistry.terminateBySid(lt.sid()); // 해당 OIDC 세션과 연결된 앱 세션 종료

} else {

sessionRegistry.terminateAllForUser(lt.sub()); // 사용자의 모든 앱 세션 종료

}

tokenStore.revokeRefreshTokens(lt.sub(), lt.sid()); // 계층3 정리

return ResponseEntity.ok().build(); // 200 OK — 실패 시 IdP가 재시도할 수 있게 5xx

}

구현의 숨은 난점은 **sid와 앱 세션의 매핑**입니다. 로그인 시점에 ID 토큰의 sid 클레임을 앱 세션과 함께 저장해 두어야, 나중에 logout token의 sid로 역으로 찾을 수 있습니다. 스테이트리스 JWT 세션만 쓰는 앱이라면 "끊을 서버 세션" 자체가 없으므로, 거부 목록(denylist) 같은 상태를 도입해야 back-channel logout이 의미를 가집니다. 로그아웃은 본질적으로 스테이트풀한 작업입니다.

Back-Channel의 한계

만능은 아닙니다.

- RP 백엔드가 IdP에서 **도달 가능한 네트워크 위치**에 있어야 합니다(방화벽 내부 앱은 곤란).

- IdP가 RP 수만큼 POST를 보내야 하므로, RP가 많고 일부가 느리면 로그아웃 지연이 생깁니다. IdP의 타임아웃/병렬화 설정이 중요합니다.

- 전달 실패 시의 재시도 정책이 IdP 구현마다 다르고, 보장은 at-most-once에 가깝습니다. **백업 수단(짧은 세션 + 주기적 토큰 재검증)과 반드시 병행**해야 합니다.

세 가지 OIDC 로그아웃 스펙 비교

여기까지의 내용을 한 표로 정리합니다.

| 기준 | Session Management (iframe 폴링) | Front-Channel Logout | Back-Channel Logout |

| --- | --- | --- | --- |

| 전파 매체 | 브라우저 (OP iframe 상태 폴링) | 브라우저 (숨겨진 iframe 로드) | 서버 간 HTTP POST |

| 서드파티 쿠키 의존 | 있음 — 치명적 | 있음 — 치명적 | 없음 |

| 전달 확인/재시도 | 불가 | 불가 | 가능 (구현 의존) |

| RP 구현 부담 | 프런트엔드 폴링 로직 | 로그아웃 URL 1개 | 서명 검증 + 세션 매핑 |

| 브라우저 닫힘/이탈에 강한가 | 아니오 | 아니오 | 예 |

| 방화벽 내부 RP | 동작 | 동작 | 곤란 |

| 2026년 권장도 | 사용 중단 | 사용 중단 | 표준 선택지 |

SAML SLO — 표준은 있으나 현실은 험난

SAML 2.0에도 [Single Logout Profile](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf)이 있습니다. LogoutRequest/LogoutResponse 메시지를 Redirect(브라우저 경유) 또는 SOAP(서버 간) 바인딩으로 주고받는 구조입니다.

xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"

ID="_8f2b3c4d" Version="2.0"

IssueInstant="2026-06-12T09:30:00Z"

Destination="https://app-a.example.com/saml/slo">

f3a8c2e1-9b47-4d6a-8c21-0e5f7a9b3d44

현실적인 문제들입니다.

- **Redirect 바인딩은 릴레이 경주**: SP1 → IdP → SP2 → IdP → SP3처럼 브라우저가 모든 참여자를 순차 방문합니다. 중간의 SP 하나가 응답하지 않거나 사용자가 창을 닫으면 거기서 체인이 끊기고, 뒤의 SP들은 로그아웃을 영영 모릅니다.

- **SOAP 바인딩은 지원 부재**: 서버 간 통보라는 점에서 back-channel과 유사하지만, 실제로 SOAP SLO를 구현한 SP는 드뭅니다. 상용 SaaS 다수가 SAML 로그인은 지원해도 SLO는 미지원이거나 형식적으로만 지원합니다.

- **업계의 묵시적 합의**: 그래서 많은 엔터프라이즈 환경이 SAML SLO를 켜지 않고, "IdP 세션 타임아웃 + 앱별 로그아웃"으로 타협합니다. SAML 연동에서 SLO 동작을 계약 요건으로 받았다면, 반드시 양쪽 제품의 실제 지원 수준을 PoC로 확인해야 합니다.

Keycloak에서의 로그아웃 설정

Keycloak(26.6 기준)은 위의 모든 메커니즘을 지원합니다. 클라이언트 단위로 설정합니다.

OIDC 클라이언트 설정 포인트

관리 콘솔의 클라이언트 설정에서:

- **Front channel logout**: on/off와 Front-channel logout URL. 앞서 설명한 이유로 끄는 것을 권장합니다.

- **Backchannel logout URL**: RP의 back-channel 수신 엔드포인트.

- **Backchannel logout session required**: logout token에 sid를 포함할지. 켜는 것을 권장합니다(세션 단위의 정밀한 로그아웃).

- **Backchannel logout revoke offline sessions**: 오프라인 세션(장수명 refresh)까지 회수할지.

kcadm CLI로 표현하면 다음과 같습니다.

클라이언트의 back-channel logout 설정

kcadm.sh update clients/CLIENT-UUID -r saas-prod \

-s 'attributes."backchannel.logout.url"=https://app-a.example.com/oidc/back-logout' \

-s 'attributes."backchannel.logout.session.required"=true' \

-s 'attributes."backchannel.logout.revoke.offline.tokens"=false' \

-s 'frontchannelLogout=false'

세션·토큰 수명과의 조합

로그아웃 전파는 실패할 수 있으므로, 세션/토큰 수명이 안전망입니다. realm 설정에서:

kcadm.sh update realms/saas-prod \

-s ssoSessionIdleTimeout=1800 \

-s ssoSessionMaxLifespan=28800 \

-s accessTokenLifespan=300 \

-s offlineSessionIdleTimeout=2592000

- **accessTokenLifespan은 짧게(5분 내외)**: back-channel logout이 refresh token을 죽여도 access token은 만료까지 삽니다. 이 창을 줄이는 유일한 방법은 수명 단축이거나, 리소스 서버가 매 요청 introspection을 하는 것입니다(성능 비용과의 트레이드오프).

- **관리자 강제 로그아웃**: Admin API로 특정 사용자의 전체 세션을 즉시 끊을 수 있습니다. 이때 back-channel 설정이 되어 있는 클라이언트들에는 logout token이 발송됩니다.

특정 사용자의 모든 세션 강제 종료

kcadm.sh create users/USER-UUID/logout -r saas-prod

세션 고정과 토큰 잔존 — 로그아웃의 보안 리스크

로그아웃이 불완전하면 다음 공격/리스크 면이 열립니다.

- **공용 PC 시나리오**: 사용자는 "로그아웃했다"고 믿고 자리를 떠났지만 IdP SSO 세션이 살아 있다면, 다음 사람이 같은 브라우저에서 앱에 접근하는 순간 자동 로그인됩니다. SSO 환경에서 가장 자주 보고되는 실사고 유형입니다.

- **토큰 잔존**: 로그아웃 후에도 살아 있는 access token은 그 수명만큼 API 접근이 가능합니다. refresh token 회수가 빠진 구현은 사실상 로그아웃이 없는 것과 같습니다. [RFC 7009 (Token Revocation)](https://datatracker.ietf.org/doc/html/rfc7009) 엔드포인트 호출을 로그아웃 절차에 포함하십시오.

- **로그아웃 CSRF**: 공격자가 피해자를 강제로 로그아웃시키는 것은 사소해 보이지만, 피싱 로그인 화면으로 유도하는 전 단계로 쓰일 수 있습니다. id_token_hint 검증과 확인 화면, post_logout_redirect_uri 화이트리스트가 방어선입니다.

- **세션 고정의 역방향 교훈**: 로그인 시 세션 ID를 재발급하듯, 로그아웃 시에는 세션 식별자와 연결된 모든 파생 상태(CSRF 토큰, 캐시된 권한)를 함께 파기해야 합니다.

모바일 앱의 로그아웃

모바일은 브라우저 세션 모델이 통하지 않는 별세계입니다.

- 모바일 앱의 "로그인 상태"는 보통 Keychain/Keystore에 저장된 refresh token입니다. 로그아웃은 (1) 로컬 토큰 삭제, (2) 서버 측 token revocation 호출, 두 가지를 모두 해야 합니다. 로컬 삭제만 하는 앱이 의외로 많은데, 탈취된 토큰이 서버에서 여전히 유효하다는 뜻입니다.

- 시스템 브라우저(ASWebAuthenticationSession, Custom Tabs)로 로그인한 경우 IdP SSO 쿠키가 시스템 브라우저에 남습니다. 완전한 로그아웃을 원하면 RP-Initiated Logout URL을 같은 메커니즘으로 한 번 열어 IdP 세션도 끊어야 합니다.

- back-channel logout token을 모바일 앱이 직접 받을 수는 없으므로(서버가 아니므로), 푸시 알림이나 다음 API 호출 시의 401 처리로 "강제 로그아웃됨"을 감지하게 설계합니다.

모바일 로그아웃 체크리스트

[ ] 로컬 refresh/access token 삭제 (Keychain/Keystore)

[ ] POST /revoke 로 서버 측 토큰 폐기 (RFC 7009)

[ ] 필요 시 RP-Initiated Logout으로 IdP 쿠키 정리

[ ] 앱 내 캐시된 사용자 데이터/암호화 키 정리

[ ] 서버 발 강제 로그아웃의 수신 경로 (push 또는 401 핸들링)

글로벌 로그아웃 vs 단일 앱 로그아웃 — UX 결정

기술이 준비되어도 "로그아웃 버튼이 무엇을 해야 하는가"는 제품 결정입니다.

| 옵션 | 동작 | 적합한 맥락 |

| --- | --- | --- |

| 단일 앱 로그아웃 | 이 앱의 세션만 종료, IdP 세션 유지 | 직장 포털류 — 다른 업무 앱은 계속 써야 함 |

| 글로벌 로그아웃 | IdP 세션 + 전체 RP 세션 종료 | 공용 PC, 보안 민감 환경, "모든 기기에서 로그아웃" |

| 선택형 | 로그아웃 시 사용자에게 범위 질문 | 혼합 환경 — 단, UX 마찰 |

권장 패턴은 다음과 같습니다.

- 일반 "로그아웃" 버튼은 **글로벌 로그아웃**(RP-Initiated → IdP 세션 종료 → back-channel 전파)으로 연결합니다. 사용자의 멘탈 모델("로그아웃했으면 끝났다")과 일치하는 것이 사고를 줄입니다.

- 단일 앱 로그아웃이 필요한 경우는 "계정 전환" 같은 별도 동선으로 분리합니다.

- 보안 설정 페이지에 "모든 기기에서 로그아웃"(Admin API 기반 전체 세션 폐기)을 별도 제공하면 계정 탈취 대응 UX가 완성됩니다.

테스트 매트릭스

로그아웃은 회귀가 잦은 영역이므로 매트릭스 기반 자동화가 필수입니다.

| # | 시나리오 | 기대 결과 |

| --- | --- | --- |

| 1 | 앱 A 로그아웃 후 앱 A 재접근 | 재인증 요구 |

| 2 | 앱 A 로그아웃 후 앱 B 접근 (글로벌 정책) | 재인증 요구 (back-channel 전파 확인) |

| 3 | 앱 A 로그아웃 후 5분 내 기존 access token으로 API 호출 | 정책에 따라 401 또는 수명 내 허용 — 문서화된 대로 |

| 4 | 로그아웃 후 refresh token으로 갱신 시도 | 401/400 invalid_grant |

| 5 | IdP 관리자 강제 세션 종료 | 모든 RP에 logout token 발송, 앱 세션 종료 |

| 6 | RP back-logout 엔드포인트 다운 중 로그아웃 | IdP 로그아웃은 성공, 복구 후 해당 RP 세션은 idle timeout으로 소멸 |

| 7 | logout token 리플레이 (동일 jti 재전송) | 거부 |

| 8 | nonce가 포함된 위조 logout token | 거부 |

| 9 | post_logout_redirect_uri에 미등록 URL | 거부 또는 무시 |

| 10 | 서드파티 쿠키 차단 브라우저에서 전체 플로우 | back-channel 경로로 정상 동작 |

| 11 | 모바일: 로그아웃 후 탈취 가정 토큰으로 API 호출 | revocation 반영으로 401 |

| 12 | 로그아웃 직후 뒤로 가기로 개인화 페이지 접근 | 캐시 미노출 (no-store 확인) |

E2E 자동화 골격 예시입니다.

#!/usr/bin/env bash

set -euo pipefail

시나리오 4: 로그아웃 후 refresh 거부 확인

TOKENS=$(curl -sf -X POST "$IDP/realms/saas-prod/protocol/openid-connect/token" \

-d "grant_type=password&client_id=e2e&username=tester&password=$E2E_PW")

RT=$(echo "$TOKENS" | jq -r .refresh_token)

IDT=$(echo "$TOKENS" | jq -r .id_token)

RP-Initiated Logout

curl -sf "$IDP/realms/saas-prod/protocol/openid-connect/logout?id_token_hint=$IDT" > /dev/null

refresh 시도는 실패해야 함

STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \

"$IDP/realms/saas-prod/protocol/openid-connect/token" \

-d "grant_type=refresh_token&client_id=e2e&refresh_token=$RT")

test "$STATUS" = "400" && echo "PASS: refresh rejected after logout"

안티패턴 모음 — 이렇게 하면 사고가 납니다

실제 코드 리뷰와 침투 테스트에서 반복적으로 발견되는 로그아웃 안티패턴입니다.

**안티패턴 1 — 프런트엔드 전용 로그아웃**

// 이것은 로그아웃이 아닙니다

function logout() {

localStorage.removeItem('access_token');

localStorage.removeItem('refresh_token');

window.location.href = '/login';

}

토큰의 로컬 사본만 지웠을 뿐, 서버 측에서 토큰은 여전히 유효하고 IdP 세션도 살아 있습니다. 브라우저 히스토리나 XSS로 토큰을 확보한 공격자에게는 아무 영향이 없습니다. 반드시 revocation 엔드포인트 호출과 RP-Initiated Logout 리다이렉트가 동반되어야 합니다.

**안티패턴 2 — 로그아웃을 GET 링크로 노출**

확인 절차 없는 GET /logout 링크는 img 태그 한 줄로 피해자를 강제 로그아웃시킬 수 있게 합니다(로그아웃 CSRF). POST + CSRF 토큰, 또는 id_token_hint 검증을 거치는 표준 플로우를 사용해야 합니다.

**안티패턴 3 — 스테이트리스 JWT 세션과 "로그아웃 됨" 가정**

서버 세션 없이 JWT만으로 인증 상태를 관리하면서 로그아웃 API가 200을 반환하는 구현입니다. 실제로는 아무것도 무효화되지 않았습니다. denylist(jti 기준), 토큰 버전 클레임, 또는 짧은 수명 + refresh 회수 중 하나는 반드시 있어야 합니다.

**안티패턴 4 — back-channel 엔드포인트의 무인증 신뢰**

logout_token 서명 검증 없이 sub만 읽어 세션을 끊는 구현은, 위조 토큰으로 임의 사용자를 로그아웃시키는 DoS 통로가 됩니다. 서명·iss·aud·events·nonce 부재까지 전체 검증이 필수입니다.

**안티패턴 5 — 로그아웃 후 캐시 잔존**

CDN이나 브라우저 캐시에 남은 개인화 페이지가 로그아웃 후에도 뒤로 가기로 노출되는 문제입니다. 인증된 응답에는 Cache-Control private/no-store를 일관 적용해야 합니다.

2026년 브라우저 프라이버시 변화가 남긴 것

정리하면, 브라우저의 프라이버시 강화(서드파티 쿠키 기본 차단, 파티셔닝된 스토리지, bounce tracking 완화)는 SSO 로그아웃 설계에 다음 영향을 남겼습니다.

1. **front-channel logout과 OIDC Session Management(iframe 폴링) 계열은 신규 설계에서 제외** — 크로스 도메인에서 동작을 보장할 수 없습니다.

2. **back-channel logout이 유일한 신뢰 가능한 전파 수단** — 단, at-most-once 전달임을 받아들이고 짧은 토큰 수명과 조합해야 합니다.

3. **FedCM 같은 브라우저 중재 신원 API**가 서드파티 쿠키 이후의 세션 신호를 일부 대체하는 방향으로 발전 중입니다. 로그인 상태 공유와 로그아웃 신호의 미래는 브라우저 API와의 협업 쪽으로 이동하고 있으므로, 인증 계층을 추상화해 두면 이행이 쉬워집니다.

4. **궁극의 안전망은 수명 설계** — 어떤 전파 메커니즘도 100%가 아니므로, "전파가 모두 실패해도 N분 뒤에는 무조건 만료"가 보장되는 access token 수명과 세션 idle timeout이 마지막 방어선입니다.

마치며

로그아웃은 분산 상태 무효화 문제이며, 완벽한 해법은 없고 좋은 트레이드오프만 있습니다. 설계 원칙을 요약합니다.

1. 세션 3계층(IdP 세션, 앱 세션, 토큰)을 명시적으로 모델링하고, 로그아웃이 각 계층에 무엇을 하는지 문서화하십시오.

2. 신규 설계는 RP-Initiated Logout + Back-Channel Logout 조합이 기본입니다. front-channel은 2026년 브라우저 환경에서 신뢰할 수 없습니다.

3. SAML SLO는 상대 제품의 실지원 수준을 검증하기 전에는 약속하지 마십시오.

4. 전파는 실패한다고 가정하고, 짧은 access token 수명 + refresh 회수 + idle timeout을 안전망으로 두십시오.

5. 로그아웃 UX(글로벌 vs 단일 앱)는 보안 결정입니다. 사용자의 멘탈 모델과 일치시키고, "모든 기기에서 로그아웃"을 제공하십시오.

6. 테스트 매트릭스를 CI에 넣으십시오. 로그아웃 회귀는 조용히 발생하고, 사고가 나서야 발견됩니다.

참고 자료

- [OpenID Connect RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)

- [OpenID Connect Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html)

- [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html)

- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)

- [SAML 2.0 Profiles (Single Logout Profile 포함, OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf)

- [SAML 2.0 Core Specification (OASIS)](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)

- [RFC 7009 — OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009)

- [RFC 7519 — JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [Keycloak Documentation](https://www.keycloak.org/documentation)

- [Keycloak Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [FedCM (Federated Credential Management) — W3C](https://www.w3.org/TR/fedcm/)

현재 단락 (1/229)

SSO 프로젝트에서 가장 늦게 발견되고 가장 오래 끄는 이슈는 의외로 로그아웃입니다. 로그인은 "한 곳에서 인증하면 모두 열린다"는 단방향 흐름이라 설계가 명쾌합니다. 그러나 로그...

작성 글자: 0원문 글자: 12,946작성 단락: 0/229