5.5 KiB
id, kind, slug, title, topic, project, status, version, basisVersion, studio
| id | kind | slug | title | topic | project | status | version | basisVersion | studio |
|---|---|---|---|---|---|---|---|---|---|
| bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07 | CONCEPT | browser-credential-storage | 브라우저가 credential을 보관하는 위치와 그 성질 | OAuth/OIDC 인증 경계 | KeyCloak Patterns | 게시 전 | 4 | Keycloak 26.7.0 · oidc-client-ts · oauth2-proxy 7.15.2 | 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만 두는 구조의 설계 항목이다.
본문
네 위치의 성질
| 위치 | 새로고침 뒤 | JavaScript가 읽나 | 요청에 자동으로 붙나 |
|---|---|---|---|
| JavaScript memory | 초기화 | 읽는다 | 붙지 않는다 |
| Session Storage | 탭이 살아 있으면 유지 | 읽는다 | 붙지 않는다 |
| Local Storage | 유지 | 읽는다 | 붙지 않는다 |
| HttpOnly cookie | 만료까지 유지 | 읽지 못한다 | 붙는다 |
자동으로 붙는다는 성질이 cookie를 credential로 쓸 때 CSRF 검증이 필요해지는 이유다.
userStore와 stateStore를 나눈다
oidc-client-ts의 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을 관측할 수 있다.
이 구성에서 관측한 두 결과는 다음과 같다.
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 기본값으로 옮겨 적지 않는다.