--- 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만 두는 구조의 설계 항목이다. ## 본문 ## 네 위치의 성질 | 위치 | 새로고침 뒤 | 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 기본값으로 옮겨 적지 않는다.