The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
5.6 KiB
Markdown
103 lines
5.6 KiB
Markdown
---
|
|
id: bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07
|
|
kind: CONCEPT
|
|
slug: browser-credential-storage
|
|
title: 브라우저가 credential을 보관하는 위치와 그 성질
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 전
|
|
version: 4
|
|
basisVersion: Keycloak 26.7.0 · oidc-client-ts · oauth2-proxy 7.15.2
|
|
studio: "https://hyeonworks.com/studio/documents/bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07/edit"
|
|
---
|
|
|
|
# 브라우저가 credential을 보관하는 위치와 그 성질
|
|
|
|
브라우저에는 JavaScript memory, Session Storage, Local Storage, cookie가 있고 각각 수명과 접근 경로가 다르다. 어떤 credential이 어디에 있는지에 따라 새로고침 뒤 남는 것, JavaScript가 읽을 수 있는 것, 요청에 자동으로 붙는 것이 갈린다.
|
|
|
|
## 관계
|
|
|
|
- **OAuth Token과 Application Session을 구분하는 기준**
|
|
여기 있는 값들에 각각 다른 이름을 쓰는 기준이다.
|
|
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
|
memory-only 구성을 실제로 확인한 기록이다.
|
|
- **BFF 인증 구조 설계 기준**
|
|
브라우저에 session cookie만 두는 구조의 설계 항목이다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 네 위치의 성질
|
|
|
|
| 위치 | 새로고침 뒤 | JavaScript가 읽나 | 요청에 자동으로 붙나 |
|
|
|---|---|---|---|
|
|
| JavaScript memory | 초기화 | 읽는다 | 붙지 않는다 |
|
|
| Session Storage | 탭이 살아 있으면 유지 | 읽는다 | 붙지 않는다 |
|
|
| Local Storage | 유지 | 읽는다 | 붙지 않는다 |
|
|
| HttpOnly cookie | 만료까지 유지 | 읽지 못한다 | 붙는다 |
|
|
|
|
자동으로 붙는다는 성질이 cookie를 credential로 쓸 때 CSRF 검증이 필요해지는 이유다.
|
|
|
|
## userStore와 stateStore를 나눈다
|
|
|
|
oidc-client-ts의 `UserManager`는 두 저장소를 따로 받는다.
|
|
|
|
```text label="AP1의 UserManager 저장소 설정"
|
|
userStore = InMemoryWebStorage
|
|
stateStore = sessionStorage
|
|
```
|
|
|
|
`userStore`는 로그인 뒤 `User`와 token set을 보관한다. `stateStore`는 redirect를 건너야 하는 authorization transaction을 보관한다.
|
|
|
|
두 저장소의 내용도 성격이 다르다.
|
|
|
|
| 저장소 | 들어가는 것 | 언제까지 필요한가 |
|
|
|---|---|---|
|
|
| userStore | `User`, access·refresh·ID token, expiry, profile | 로그인 상태가 유지되는 동안 |
|
|
| stateStore | `state`, PKCE verifier | callback 처리가 끝날 때까지 |
|
|
|
|
`state`와 verifier는 Keycloak 왕복을 건너야 하므로 memory에 둘 수 없다. 이 값이 Session Storage에 있는 것과 token이 Web Storage에 있는 것은 다른 설정이다.
|
|
|
|
## memory-only가 뜻하는 범위
|
|
|
|
`InMemoryWebStorage`는 로그인 결과를 브라우저의 영구 저장소가 아니라 실행 중 memory에만 둔다. 새로고침하면 `User`와 token이 초기화되고, Local Storage와 Session Storage에는 token 복사본이 남지 않는다.
|
|
|
|
memory-only는 persistent storage에 쓰지 않는다는 뜻이다. 실행 중 script가 응답이나 지역 변수를 읽을 수 없다는 뜻은 아니다. 브라우저 fetch를 hook하면 API 호출의 Bearer access token을 관측할 수 있다.
|
|
|
|
이 구성에서 관측한 두 결과는 다음과 같다.
|
|
|
|
```text label="함께 읽어야 하는 두 결과"
|
|
Local Storage · Session Storage → access token 문자열 없음
|
|
실행 중 fetch hook → Authorization: Bearer 관측됨
|
|
```
|
|
|
|
AP2도 같은 구분이 필요하다. `/token/access` 응답의 access token은 JavaScript 지역 변수로 들어갔다가 다음 요청 헤더가 된다. 세 경계를 지나는 동안 persistent storage에는 쓰이지 않는다.
|
|
|
|
## HttpOnly cookie
|
|
|
|
HttpOnly는 JavaScript가 cookie 값을 직접 읽지 못하게 하는 속성이다. `document.cookie`로 조회되지 않지만 브라우저는 요청마다 붙여 보낸다.
|
|
|
|
AP2의 `AP2_SESSION`, AP3의 `AP3_SESSION`, AP4의 `AP4_SESSION`이 모두 HttpOnly다. 브라우저 JavaScript에 OAuth token을 전달하지 않는 구조에서도 이 cookie는 남는다. 브라우저에 없는 것은 애플리케이션이 쓰는 OAuth token이고, 인증 상태 자체는 이 cookie로 남아 있다.
|
|
|
|
Keycloak 도메인의 SSO cookie도 별도로 존재할 수 있다. 애플리케이션 memory의 `User`가 사라진 것과 IdP session이 끝난 것은 다른 사건이다.
|
|
|
|
## opaque cookie
|
|
|
|
opaque는 내부 값을 브라우저가 해석하지 않고 그대로 돌려준다는 뜻이다.
|
|
|
|
AP2와 AP3의 session cookie는 server-side 상태를 찾는 열쇠다. 실제 access token과 refresh token은 authorized-client store에 있고 cookie 안에는 없다. cookie가 token map을 직렬화한다고 설명하면 구현이 틀리게 된다.
|
|
|
|
AP4에서 `session-cookie-minimal=true`를 쓰면 server-side session store 없이 edge가 필요한 최소 정보만 cookie 자체에 담는다. access·refresh·ID token은 여기에 들어가지 않는다. 그래서 AP4가 refresh token을 지속 보관한다고 말할 수 없다.
|
|
|
|
AP2와 AP3의 cookie는 server-side 상태를 찾는 열쇠이고, AP4의 cookie는 최소 상태를 담은 값이다. 두 cookie를 같은 문장으로 설명하지 않는다.
|
|
|
|
## 학습 환경의 cookie 속성을 일반화하지 않는다
|
|
|
|
지금 구성은 cookie 속성과 redirect를 눈으로 확인하려고 HTTPS가 아닌 HTTP를 쓴다. 그래서 `AP4_SESSION`의 `Secure`가 `false`다. 운영 HTTPS에서는 먼저 `Secure=true`를 설정해야 한다.
|
|
|
|
`Secure`, Domain, 만료를 로컬 YAML이 고정하지 않는 구성도 있다. 여기서 관측한 값을 운영 cookie 기본값으로 옮겨 적지 않는다.
|
|
|
|
<!-- body:end -->
|