Files
document-haness/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

5.3 KiB

id, kind, slug, title, topic, topicName, project, status, version, basisVersion, studio
id kind slug title topic topicName project status version basisVersion studio
5c8f12d5-1ead-469b-8e91-2de69401df48 CONCEPT cookie-auth-csrf Cookie로 인증하는 요청에서 CSRF token이 하는 일 oauth-oidc-auth-boundary OAuth/OIDC 인증 경계 KeyCloak Patterns 게시 전 4 Spring Security 6 CSRF · AP3 BFF 구성 https://hyeonworks.com/studio/documents/5c8f12d5-1ead-469b-8e91-2de69401df48/edit

Cookie로 인증하는 요청에서 CSRF token이 하는 일

session cookie는 브라우저가 요청마다 자동으로 붙인다. 그래서 상태를 바꾸는 요청이 사용자의 의도인지 서버가 따로 확인해야 한다. CSRF token이 그 확인이고, SameSite는 브라우저가 cookie를 언제 보낼지 정하는 별도의 정책이다.

관계

  • BFF 인증 구조 설계 기준 이 확인이 필요한 구조의 설계 항목이다.
  • Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정 이 동작을 실제로 재현한 기록이다.
  • OAuth Token과 Application Session을 구분하는 기준 session cookie와 CSRF token은 서로 다른 값이다.

본문

cookie가 credential이 되면 생기는 일

브라우저가 OAuth token을 받지 않는 구조에서도 인증 상태는 남는다. BFF는 HttpOnly session cookie로 로그인 상태를 찾는다.

이 cookie는 브라우저가 자동으로 붙인다. 다른 사이트가 만든 요청에도 붙을 수 있다는 뜻이다. GET /bff/api/me만 보면 이 문제가 드러나지 않으므로 상태를 바꾸는 요청을 따로 봐야 한다.

token을 받아 오는 요청

브라우저가 먼저 CSRF material을 요청한다.

GET http://localhost:8083/bff/csrf
Accept: application/json
Cookie: AP3_SESSION=<opaque-session-id>

CookieCsrfTokenRepository.withHttpOnlyFalse()는 JavaScript가 읽을 수 있는 XSRF-TOKEN cookie를 path /에 만든다. controller는 다음 JSON을 반환한다.

{
  "headerName": "X-XSRF-TOKEN",
  "parameterName": "_csrf",
  "token": "<xor-masked-csrf-token>"
}

body의 token과 cookie의 값은 다르다

같은 CSRF material이 세 자리에 서로 다른 형태로 놓인다.

위치
응답 body의 token XOR와 Base64로 mask된 값
XSRF-TOKEN cookie raw 값
POST의 X-XSRF-TOKEN 헤더 cookie와 같은 raw 값

XorCsrfTokenRequestAttributeHandler가 request attribute용 token을 mask하기 때문에 controller JSON에는 masked 값이 보인다. SPA는 JSON에서 headerName만 읽고, 실제 값은 document.cookie에서 raw XSRF-TOKEN을 찾아 쓴다.

SpaCsrfTokenRequestHandler가 이 조합을 맞춘다. expected 헤더가 있으면 plain resolver로 제출된 raw token을 읽고, 없으면 XOR resolver 경로를 쓴다.

응답 JSON의 token을 그대로 헤더에 복사하면 값이 맞지 않아 403이 된다. 노출 값과 제출 값이 다를 수 있다는 것을 클라이언트 코드가 알아야 한다.

검증이 controller보다 먼저 일어난다

정상 상태 변경 요청은 다음과 같다.

POST http://localhost:8083/bff/api/preferences
Content-Type: application/x-www-form-urlencoded
Cookie: AP3_SESSION=<opaque-session-id>; XSRF-TOKEN=<raw-csrf-token>
X-XSRF-TOKEN: <same-raw-csrf-token>

theme=dark

Spring CSRF filter가 repository의 expected token과 제출된 헤더를 비교한다. 헤더가 없거나 값이 맞지 않으면 controller는 실행되지 않고 403이 된다. 검증 지점이 controller 앞이라 endpoint를 추가해도 같은 filter를 지난다.

SameSite가 정하는 것과 CSRF token이 정하는 것

SameSite CSRF token
누가 판단하나 브라우저 서버
무엇을 정하나 cookie를 보낼지 요청을 받아들일지
언제 작동하나 요청을 만들 때 요청을 처리할 때

port가 달라도 site 계산상 같은 경우가 있어서, SameSite가 cookie를 빼지 않는 요청에도 CSRF 검증이 걸려야 한다.

네 가지 입력에서 cookie와 CSRF 검증이 각각 어떻게 동작하는지는 다음과 같다.

입력 cookie 동작 CSRF 동작 결과
same-origin, CSRF 헤더 없음 session cookie 붙음 token 부재로 거부 403
same-origin, raw cookie와 헤더 일치 session cookie 붙음 token 일치 200
다른 port지만 same-site, 헤더 없음 cookie가 붙을 수 있음 token 부재로 거부 403
cross-site POST SameSite=Lax로 cookie 제외 이 지점 이후는 고정하지 않음 cookie omission이 확인 지점

마지막 줄에서 확인하는 것은 최종 status가 아니라 cookie가 빠졌는지다.

CSRF가 XSS를 대신하지 않는다

브라우저에 OAuth token을 주지 않아도 same-origin 악성 script는 피해자 session으로 BFF endpoint를 부를 수 있다. JavaScript가 읽을 수 있는 XSRF-TOKEN도 같이 읽을 수 있다.

이 구조가 줄이는 것은 access·refresh token 원문이 script에서 유출되어 다른 client나 직접 API 호출에 재사용되는 범위다. CSP, output encoding, 의존성 무결성, 애플리케이션 인가는 별도 방어선으로 남는다.