- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중 17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로 tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8 - 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다 - 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference) - 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다. 삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
10 KiB
id, kind, slug, title, topic, topicName, project, status, version, verifiedOn, studio, public, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | version | verifiedOn | studio | public | sourceRevision | source | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 66c18e42-116c-459f-86bd-b7e4bf394866 | REFERENCE | oauth-token-application-session-boundary | OAuth Token과 Application Session을 구분하는 기준 | oauth-oidc-auth-boundary | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 중 | 33 | 2026-08-30 | https://hyeonworks.com/studio/documents/66c18e42-116c-459f-86bd-b7e4bf394866/edit | https://hyeonworks.com/references/oauth-token-application-session-boundary | keycloak-patterns-lab@2026-08 |
|
OAuth Token과 Application Session을 구분하는 기준
IdP의 SSO 세션, 액세스 토큰, 리프레시 토큰, 애플리케이션의 세션 쿠키, 인증 프록시의 세션 쿠키는 만드는 쪽과 쓰는 쪽이 각각 다르고 유효 시간도 서로 다르다.
액세스 토큰이 만료되었다고 해서 애플리케이션 세션이나 IdP의 SSO 세션까지 같이 만료된 것은 아니고, 애플리케이션 세션을 삭제했다고 해서 IdP의 SSO 세션이나 이미 발급된 토큰이 사라지는 것도 아니다.
그래서 어떤 세션과 토큰이 아직 살아 있는지, 무엇이 만료되었는지, 로그아웃할 때 어떤 상태를 삭제하거나 무효화해야 하는지를 종류마다 나눠서 확인한다.
관계
- SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계 JavaScript 메모리의 OAuth 토큰과 Keycloak SSO 세션을 구분한 사례다.
- Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출 같은 요청 안에서 세션 쿠키와 액세스 토큰이 함께 움직인다.
- Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 BFF에서는 세션 쿠키, JavaScript가 읽는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 토큰, 서버 쪽 OAuth 토큰을 서로 다른 용도로 쓴다.
- Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유 Forward-Auth에서는 업스트림이 JWT를 직접 검증하지 않고, 프록시 세션을 근거로 엣지가 만든 사용자 정보 헤더를 쓴다.
목적
SPA(Single Page Application, 단일 페이지 애플리케이션), Mediator, BFF(Backend For Frontend) 구성에서는 Resource Server가 액세스 토큰을 검증한 뒤 JWT의 클레임에서 사용자 정보를 얻을 수 있다. Forward-Auth 구성에서는 애플리케이션이 인증 프록시가 전달한 사용자 정보 헤더를 쓴다. 두 경로가 같은 사용자 이름을 내놓더라도 한쪽은 액세스 토큰을 검증해서 얻은 값이고, 다른 한쪽은 신뢰하기로 정한 프록시가 전달한 값이다.
로그아웃과 만료 처리에서도 같은 구분이 필요하다. IdP의 SSO 세션, 액세스 토큰과 리프레시 토큰, 애플리케이션 세션은 서로 다른 주체가 관리하고 수명도 다르다. 그래서 로그아웃할 때 무엇을 삭제하거나 무효화할지, 어떤 자격 증명이 만료되었을 때 어떤 상태를 계속 쓸 수 있는지를 각각 나눠서 설계한다.
규칙
1. 인증 상태를 종류별로 구분해서 기록한다
IdP SSO 세션, OAuth 액세스 토큰, OAuth 리프레시 토큰, 애플리케이션 세션 쿠키, 프록시 세션 쿠키는 만드는 주체와 쓰는 목적이 저마다 다른 별개의 상태다.
로그와 진단 정보에도 어떤 상태를 확인한 것인지 구체적으로 적는다. 로그인 상태가 만료되었다고만 남기는 대신 애플리케이션 세션이 만료되었다, 액세스 토큰이 만료되었다, 프록시 세션이 없다처럼 실제 세션이나 토큰의 종류를 밝혀서 적는다.
2. 발급하는 쪽과 사용하는 쪽으로 구분한다
액세스 토큰, 애플리케이션 세션 쿠키, 프록시 세션 쿠키는 각각 발급하는 주체와 사용하는 주체가 다르다.
액세스 토큰은 IdP가 발급하고 Resource Server가 요청을 처리할 때 검증한다. 애플리케이션 세션 쿠키는 애플리케이션이 발급하고, 이후 브라우저가 보낸 쿠키로 애플리케이션이 자기 로그인 세션을 찾는 데 쓴다. 프록시 세션 쿠키는 인증 프록시가 발급하고, 이후 프록시가 인증 상태를 확인할 때 쓴다.
3. 같은 사용자라도 자격 증명마다 가리키는 상태가 다르다
애플리케이션 세션 쿠키는 서버에 저장된 세션을 찾을 세션 ID를 브라우저에 전달하는 데 쓴다. 액세스 토큰과 리프레시 토큰은 쿠키 안에 들어 있지 않고 Authorized Client 같은 별도의 서버 저장소에 보관되므로, 세션 쿠키와 OAuth 토큰 저장소는 갈라서 봐야 한다.
프록시 세션 쿠키가 반드시 같은 방식으로 동작하지는 않는다. 서버에 세션 저장소를 따로 두지 않고 인증 상태를 확인하는 데 필요한 정보를 쿠키 자체에 담은 뒤, 프록시가 그 쿠키의 유효성을 검증하는 방식으로 구성할 수도 있다. 이때 쿠키는 서버에 저장된 세션을 조회하는 세션 ID와 역할이 다르다.
4. 브라우저에 없는 것을 범위까지 적는다
브라우저 JavaScript에 OAuth 토큰을 넘기지 않는 구조에서도 브라우저에는 인증과 관련된 상태가 있을 수 있다. BFF 구조에서는 애플리케이션의 HttpOnly 세션 쿠키가 유지될 수 있고, IdP는 자기 도메인에 SSO 세션 쿠키를 유지할 수 있다.
브라우저 JavaScript에 액세스 토큰과 리프레시 토큰을 노출하지 않는다처럼 무엇이 없고 어느 범위에서 접근할 수 없는지를 적는다.
5. 영구 저장과 메모리 보관을 구분한다
OAuth 토큰을 JavaScript 메모리에만 두면 Local Storage나 Session Storage 같은 Web Storage에 토큰을 계속 저장하지 않을 수 있다. 다만 실행 중인 브라우저 JavaScript에서 토큰에 접근할 수 없다는 뜻은 아니다.
애플리케이션이 Token Endpoint의 응답을 JavaScript로 받아 처리한다면 실행 중에는 토큰 값이 JavaScript가 다루는 메모리에 있다. 같은 Origin에서 악성 스크립트가 실행될 수 있는 상황이라면 토큰 응답이나 애플리케이션이 토큰을 처리하는 경로가 공격 대상이 될 수 있으므로, Web Storage에 토큰을 저장하지 않는 것만으로는 XSS(Cross-Site Scripting, 사이트 간 스크립팅)로 토큰에 접근하는 것까지 막지 못한다.
6. 로그아웃 범위를 상태별로 적는다
애플리케이션에서 로그아웃하는 것과 IdP의 SSO 세션을 끝내는 것은 서로 다른 동작이다. 애플리케이션 세션이나 쿠키를 삭제해도 IdP의 SSO 세션은 살아 있을 수 있고, 반대로 IdP 세션을 끝내도 이미 발급된 액세스 토큰을 어떻게 처리할지는 따로 확인해야 한다.
특히 Resource Server가 Self-contained JWT 액세스 토큰을 요청마다 IdP에 물어보지 않고 스스로 검증하는 구조에서는, 이미 발급된 토큰이 로그아웃과 동시에 자동으로 무효가 되지는 않는다. Resource Server는 JWT의 서명과 만료 시간처럼 필요한 클레임을 검증하고, 토큰이 아직 유효하면 요청을 받아들일 수 있다.
그래서 이미 발급된 토큰의 상태를 한 번 더 확인하는 차단 목록(Denylist) 같은 장치를 두지 않으면, 애플리케이션 로그아웃만으로 기존 액세스 토큰을 곧바로 못 쓰게 만들 수 없다. 이런 구조에서는 액세스 토큰의 TTL을 짧게 잡아 로그아웃 뒤에 기존 토큰을 쓸 수 있는 시간을 제한하고, 리프레시 토큰과 세션은 각각의 저장 위치와 관리 주체에 맞게 따로 끝내거나 지운다.
7. 로그아웃할 자격 증명을 구체적으로 적는다
SPA에서 JavaScript 메모리에 두었던 OAuth 토큰을 지워도 Keycloak의 SSO 세션까지 끝나지는 않는다. Keycloak의 SSO 세션이 아직 유효하다면 다음에 새 인가 요청(Authorization Request)을 보냈을 때 사용자가 아이디와 비밀번호를 다시 입력하지 않고 인증 절차가 진행될 수 있다. 브라우저를 새로고침해서 메모리에 있던 토큰이 사라진 경우도 로그아웃과 같지 않다.
어느 수준까지 로그아웃할 것인지에 따라 애플리케이션 상태와 IdP의 SSO 세션을 각각 어떻게 끝낼지 정해야 한다.
Mediator나 BFF처럼 서버가 애플리케이션 세션과 Authorized Client를 함께 관리하는 구조에서는 두 상태를 정리하는 방법도 각각 적는다. 애플리케이션 세션을 무효화하는 것과 Authorized Client에 저장된 액세스 토큰과 리프레시 토큰을 지우는 것은 서로 다른 처리이므로, 로그아웃할 때 어떤 상태를 삭제하고 어떤 상태를 유지할지 따로 확인한다.
적용 조건
- 인증 상태를 표나 문서로 정리할 때
- 로그아웃과 만료 동작을 설계할 때
- 브라우저가 어떤 자격 증명을 저장하거나 전송하는지 설명할 때
- 여러 구조를 비교할 때
예외
- 하나의 요청 흐름 안에서 어떤 세션이나 토큰을 가리키는지 이미 분명하면 짧은 이름을 쓸 수 있다. 다만 문서에 처음 나올 때는 전체 이름을 먼저 적어 어떤 상태를 가리키는지 분명하게 정의하고, 이후 같은 문맥에서는 뜻이 달라지지 않는 범위에서 세션, 액세스 토큰, 리프레시 토큰처럼 줄여서 쓸 수 있다.
- IdP를 쓰지 않고 애플리케이션이 자체 로그인만 하는 구조에는 SSO 세션과 액세스 토큰, 리프레시 토큰이 없다.
예시
- IdP SSO 세션 : IdP 도메인의 쿠키이고 애플리케이션 메모리와 별개다
- 액세스 토큰 : IdP가 만들고 Resource Server가 서명과 issuer, audience를 검증한다
- 리프레시 토큰 : 새 액세스 토큰을 받는 장기 자격 증명이다
- 애플리케이션 세션 쿠키 : 서버에 있는 로그인 상태를 찾는 자격 증명이다
- 프록시 세션 쿠키 : 프록시의 인증 엔드포인트에 제시하는 최소 상태다
- CSRF 토큰 : 쿠키가 자동으로 붙는 상태 변경 요청의 의도를 확인한다
- 사용자 정보 헤더 : 엣지가 확인한 사용자 정보이고 JWT 토큰이 아니다