--- 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" sourceRevision: keycloak-patterns-lab@2026-08 source: - final/document.md#문제를-어렵게-만든-제약-브라우저에-없다 --- # 브라우저가 credential을 보관하는 위치와 그 성질 브라우저에는 JavaScript 메모리, Session Storage, Local Storage, 쿠키가 있고 각각 수명과 접근 경로가 다르다. 자격 증명(credential)을 어디에 두느냐에 따라 새로고침 뒤에 무엇이 유지되는지, JavaScript가 무엇을 읽을 수 있는지, 어떤 값이 요청에 자동으로 실리는지가 갈린다. ## 관계 - **OAuth Token과 Application Session을 구분하는 기준** 여기 있는 값들을 어떤 이름으로 갈라 부르는지 정한 기록이다. - **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계** memory-only 구성을 실제로 확인한 기록이다. - **BFF 인증 구조 설계 기준** 브라우저에 세션 쿠키만 두는 구조의 설계 항목이다. ## 본문 ## 네 위치의 성질 네 곳을 수명, JavaScript의 접근 여부, 요청 자동 첨부 세 가지로 갈라 보면 이렇다. 표의 `HttpOnly`는 JavaScript가 쿠키 값을 직접 읽지 못하게 하는 쿠키 속성인데, 나머지 성질은 뒤에서 따로 살펴보겠다. | 위치 | 새로고침 뒤 | JavaScript가 읽나 | 요청에 자동으로 붙나 | |---|---|---|---| | JavaScript 메모리 | 초기화 | 읽는다 | 붙지 않는다 | | Session Storage | 탭이 살아 있으면 유지 | 읽는다 | 붙지 않는다 | | Local Storage | 유지 | 읽는다 | 붙지 않는다 | | HttpOnly 쿠키 | 만료까지 유지 | 읽지 못한다 | 붙는다 | 네 곳 가운데 쿠키만 요청에 자동으로 붙기 때문에, 쿠키를 자격 증명으로 쓰면 그 요청이 사용자가 의도한 것인지 확인하는 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 검증이 따라붙는다. ## userStore와 stateStore를 나눈다 oidc-client-ts의 `UserManager`는 저장소를 두 개 따로 받는다. AP1은 이렇게 설정했다. ```text label="AP1의 UserManager 저장소 설정" userStore = InMemoryWebStorage stateStore = sessionStorage ``` `userStore`는 로그인 뒤 `User`와 토큰 묶음을 보관하고, `stateStore`는 리다이렉트를 건너야 하는 authorization transaction을 보관한다. | 저장소 | 들어가는 것 | 언제까지 필요한가 | |---|---|---| | userStore | `User`, access·refresh·ID token, 만료 시각, 프로필 | 로그인 상태가 유지되는 동안 | | stateStore | `state`, PKCE verifier | 콜백 처리가 끝날 때까지 | `state`와 verifier는 Keycloak 왕복을 건너야 하므로 메모리에 둘 수 없다. 이 두 값이 Session Storage에 있는 것과 토큰이 Web Storage에 있는 것은 서로 다른 설정이다. ## memory-only가 뜻하는 범위 `InMemoryWebStorage`는 로그인 결과를 브라우저의 영구 저장소가 아니라 실행 중 메모리에만 둔다. 그래서 새로고침하면 `User`와 토큰이 초기화되고, Local Storage와 Session Storage에도 토큰 복사본이 만들어지지 않는다. 이 설정은 저장 위치를 견준 뒤에 고른 것이다. Local Storage나 Session Storage에 토큰을 저장하면 새로고침은 편해지지만 노출 시간도 길어진다. HttpOnly 쿠키로 옮기는 것은 저장 위치만 바꾸면 끝나는 작업이 아니라 서버가 세션이나 토큰 중계를 맡는 AP3 계열 구조가 있어야 한다. refresh token만 mediator로 옮기는 AP2도 함께 검토했지만, 두 구조 모두 브라우저가 code를 교환하고 토큰 수명을 관리하는 모습을 가린다. AP1은 그 과정을 보여 주려고 access·refresh·ID token을 JavaScript 메모리에 두었고, 새로고침 뒤에 인증 상태를 복구하지 못하는 것을 감수했다. memory-only는 영구 저장소에 쓰지 않는다는 뜻이지, 실행 중인 스크립트가 응답이나 지역 변수를 읽을 수 없다는 뜻은 아니다. 브라우저의 `fetch`를 가로채면 API 호출에 실린 Bearer access token을 그대로 관측할 수 있다. ```text label="관측한 두 결과" Local Storage · Session Storage → access token 문자열 없음 실행 중 fetch hook → Authorization: Bearer 관측됨 ``` AP2에도 같은 구분이 필요하다. `/token/access` 응답의 access token은 JavaScript 지역 변수로 들어갔다가 다음 요청 헤더가 되고, 세 경계를 지나는 동안 영구 저장소에는 쓰이지 않는다. ## HttpOnly cookie `HttpOnly` 쿠키는 `document.cookie`로 조회되지 않지만, 브라우저는 요청마다 이 값을 자동으로 붙여 보낸다. AP2의 `AP2_SESSION`, AP3의 `AP3_SESSION`, AP4의 `AP4_SESSION`이 모두 HttpOnly다. AP3와 AP4는 브라우저 JavaScript에 OAuth 토큰을 넘기지 않는다. 브라우저를 확인하면 HttpOnly 세션 쿠키가 남아 있고 요청할 때마다 자동으로 붙는다. 그래서 이 기록에서 「브라우저에 없다」는 말은 애플리케이션이 쓰는 OAuth 토큰에만 쓴다. 인증 상태 자체는 이 쿠키가 들고 있다. Keycloak 도메인의 SSO 쿠키도 별도로 존재할 수 있다. 애플리케이션 메모리의 `User`가 사라진 것과 IdP(Identity Provider) 세션이 끝난 것은 다른 사건이다. ## opaque cookie opaque는 내부 값을 브라우저가 해석하지 않고 그대로 돌려준다는 뜻이다. AP2와 AP3의 세션 쿠키는 서버 쪽 상태를 찾는 열쇠다. 실제 access token과 refresh token은 authorized-client store에 있고 쿠키 안에는 없다. AP4에서 `session-cookie-minimal=true`를 쓰면 서버 쪽 세션 저장소 없이 엣지가 필요로 하는 최소 정보만 쿠키 자체에 담는다. access·refresh·ID token은 여기에 들어가지 않는다. 그래서 AP4가 refresh token을 지속해서 보관한다고 말할 수 없다. AP2와 AP3의 쿠키는 서버 쪽 상태를 찾는 열쇠이고, AP4의 쿠키는 최소 상태를 담은 값이다. ## 학습 환경의 cookie 속성을 일반화하지 않는다 지금 구성은 쿠키 속성과 리다이렉트를 눈으로 확인하려고 HTTPS가 아니라 HTTP를 쓰고 있어서 `AP4_SESSION`의 `Secure`가 `false`다. 운영 HTTPS에서는 먼저 `Secure=true`를 설정해야 한다. `Secure`, Domain, 만료를 로컬 YAML이 고정하지 않는 구성도 있다.