Files
document-haness/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-cookie-auth-csrf.md
T
DongHyeonkaandClaude Fable 5.1 4d50bb939a docs(keycloak): adopt the decomposition contract, fix the redirect URI, strip evaluative prose
- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중
  17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로
  tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8
- 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html
  (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다
- 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference)
- 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다.
  삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

6.7 KiB

id, kind, slug, title, topic, topicName, project, status, version, basisVersion, studio, sourceRevision, source
id kind slug title topic topicName project status version basisVersion studio sourceRevision source
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 keycloak-patterns-lab@2026-08
final/document.md#선택의-이유와-지킨-경계-ap3

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

세션 쿠키는 브라우저가 요청마다 자동으로 붙이기 때문에, 상태를 바꾸는 요청이 사용자가 의도한 것인지 서버가 따로 확인해야 한다. CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조) 토큰이 그 확인을 맡고, SameSite는 브라우저가 쿠키를 언제 보낼지 정하는 별도의 정책이다.

관계

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

본문

cookie가 credential이 되면 생기는 일

BFF(Backend for Frontend)는 브라우저에 OAuth 토큰을 내려보내지 않는다. 대신 JavaScript가 읽을 수 없는 HttpOnly 세션 쿠키 하나로 로그인한 사용자를 알아본다.

이 쿠키를 요청에 붙이는 쪽은 애플리케이션 코드가 아니라 브라우저다. 다른 사이트가 만든 요청에도 같은 쿠키가 실릴 수 있다는 뜻이다. GET /bff/api/me처럼 읽기만 하는 요청에서는 이것이 문제로 보이지 않으므로, 값을 바꾸는 요청을 따로 봐야 한다.

브라우저에 OAuth 토큰을 내려보내지 않기로 하면서 로그인 상태가 서버로 옮겨 왔고, 값을 바꾸는 요청을 가려내는 일도 그때 BFF가 맡게 됐다. 먼저 브라우저가 토큰을 받아 오는 요청부터 본다.

token을 받아 오는 요청

브라우저는 상태를 바꾸는 요청을 보내기 전에 CSRF 값부터 받아 온다.

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

CookieCsrfTokenRepository.withHttpOnlyFalse()는 JavaScript가 읽을 수 있는 XSRF-TOKEN 쿠키를 경로 /에 만든다. 컨트롤러는 다음 JSON을 반환한다.

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

body의 token과 cookie의 값은 다르다

같은 CSRF 값이 응답 본문, 쿠키, 요청 헤더 세 곳에 서로 다른 형태로 놓인다.

XorCsrfTokenRequestAttributeHandler가 요청 속성에 넣는 토큰을 XOR와 Base64로 가리기 때문에, 위 JSON에는 가려진 값이 담긴다. 쿠키 쪽은 다르다. XSRF-TOKEN에는 가리지 않은 원본 값이 들어간다. 그래서 SPA는 JSON에서 headerName만 읽고, 실제로 보낼 값은 document.cookie에서 XSRF-TOKEN을 찾아 쓴다.

위치
응답 본문의 token XOR와 Base64로 가린 값
XSRF-TOKEN 쿠키 원본 값
POST의 X-XSRF-TOKEN 헤더 쿠키와 같은 원본 값

SpaCsrfTokenRequestHandler가 이 조합을 맞춘다. 서버가 기대하는 헤더가 요청에 있으면 제출된 원본 토큰을 그대로 읽고, 없으면 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 필터가 저장소에 있는 기대값과 제출된 헤더를 비교한다. 헤더가 없거나 값이 맞지 않으면 컨트롤러는 실행되지 않고 403이 된다. 검증하는 곳이 컨트롤러보다 앞이라, 엔드포인트를 새로 추가해도 같은 필터를 지난다.

다만 이 필터가 보는 것은 CSRF 토큰뿐이다. 경로별 인가는 이 예제의 자동 테스트가 확인하는 범위 밖이라, 새 엔드포인트가 같은 필터를 지났다는 것으로 그 엔드포인트의 인가까지 확인되지는 않는다.

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

SameSite는 쿠키를 다른 사이트로 보낼지 브라우저가 정하는 정책이고, CSRF 토큰은 상태를 바꾸는 요청이 사용자의 의도인지 서버가 검증하는 애플리케이션 규약이다.

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

포트가 달라도 사이트 계산상 같은 사이트로 잡히는 경우가 있어서 둘은 서로를 대신하지 못한다. SameSite가 쿠키를 빼지 않는 요청에도 CSRF 검증이 걸려야 한다.

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

입력 쿠키 동작 CSRF 동작 결과
same-origin, CSRF 헤더 없음 세션 쿠키 붙음 토큰이 없어 거부 403
same-origin, 원본 쿠키와 헤더 일치 세션 쿠키 붙음 토큰 일치 200
다른 포트지만 same-site, 헤더 없음 쿠키가 붙을 수 있음 토큰이 없어 거부 403
cross-site POST SameSite=Lax로 쿠키 제외 이후 서버 처리는 고정하지 않음 쿠키가 빠졌는지가 확인 지점

CSRF가 XSS를 대신하지 않는다

브라우저에 OAuth 토큰을 주지 않아도 XSS가 무해해지지는 않는다. 같은 출처에서 실행되는 악성 스크립트는 피해자의 세션으로 BFF 엔드포인트를 그대로 부를 수 있고, JavaScript가 읽으라고 열어 둔 XSRF-TOKEN도 함께 읽을 수 있기 때문이다.

이 구조가 줄이는 것은 액세스 토큰과 리프레시 토큰의 원문이 스크립트로 새어 나가 다른 클라이언트나 직접 API 호출에 다시 쓰이는 범위다. CSP, 출력 인코딩, 의존성 무결성, 애플리케이션 인가는 이 구조가 대신 막아 주지 않으므로 각각 따로 세워야 한다.