不把令牌放在浏览器里的方法:BFF
한국어 원문으로 표시합니다.
한 줄 요약
SPA 가 access token 을 직접 쥐면, 그 페이지에서 도는 스크립트 하나만 오염돼도 토큰이 밖으로 나간다. BFF(Backend for Frontend) 는 토큰을 서버에만 두고 브라우저에는 HttpOnly 세션 쿠키만 준다. 훔칠 토큰이 브라우저에 아예 없게 만드는 설계다.
왜 이게 필요했나
한동안 SPA 의 표준 구성은 이랬다. 브라우저가 발급자에게서 access token 을 받아 localStorage 에 넣고, API 를 부를 때마다 Authorization: Bearer 로 붙인다. 서버는 상태가 없어 편하다. 문제는 그 토큰을 읽을 수 있는 주체가 "이 페이지에서 도는 모든 스크립트" 라는 데 있다. 광고 태그, 분석 스크립트, 오염된 npm 패키지, XSS 한 줄 — 무엇이든 토큰을 읽어 공격자 서버로 보낼 수 있고, 그 토큰은 공격자 손에서 만료까지 유효하다. 사용자가 탭을 닫아도 소용없다.
IETF 는 이 문제를 오래 다듬어 RFC 10017 OAuth 2.0 for Browser-Based Applications(초안 이름 draft-ietf-oauth-browser-based-apps)로 냈다. 여러 구성을 비교한 뒤 6.1절에 BFF 를 둔다. 핵심은 한 문장이다 — 토큰이 BFF 에만 있으니, 브라우저에서 꺼내 갈 토큰이 없다.
어떻게 동작하나
BFF 는 프런트엔드와 같은 출처에 서는 작은 서버다. 이 서버가 OAuth 클라이언트 역할을 통째로 가져간다.
브라우저 ── GET /login ─────────────▶ BFF (state 를 담은 임시 세션 발급)
브라우저 ── 302 ▶ 발급자 /authorize (로그인) ── 302 ▶ BFF /callback?code=..
BFF ─────── code + client_secret ──▶ 발급자 /token → access token
BFF: 토큰을 서버 쪽 세션에 저장, 세션 ID 재발급, HttpOnly 쿠키만 내려줌
브라우저 ── GET /bff/api/me + 쿠키 ─▶ BFF ── Authorization: Bearer ▶ 하류 API
지켜야 할 규칙은 6.1.3절에 모여 있다.
- 기밀 클라이언트. BFF 는 발급자에 자격(클라이언트 비밀)을 두고 인가 코드 그랜트를 쓴다. 코드가 주소창이나 로그에서 새어도 비밀 없이는 토큰으로 못 바꾼다.
- 쿠키.
Secure와HttpOnly는 MUST,SameSite=Strict와Path=/, 그리고 HTTP 로만 설정됐다는 뜻의__Host-Http-같은 접두는 SHOULD 다. 쿠키에는 세션 ID 만 들어가고, 그 값은 하류 API 에게 아무 의미가 없다. - 전달. BFF 는 요청에서 쿠키를 떼고 사용자의 access token 을 붙여 하류로 넘긴다. 하류 API 는 쿠키를 모르고 Bearer 토큰만 믿는다. 토큰이 없거나 죽었으면 RFC 6750 3절대로 401 과
WWW-Authenticate: Bearer를 준다. - CSRF. 브라우저가 쿠키를 자동으로 싣기 때문에 BFF 는 CSRF 방어가 MUST 다. RFC 는 SameSite=Strict, 그리고 커스텀 요청 헤더를 요구해 교차 출처 요청이 반드시 CORS preflight 를 거치게 하는 방법을 든다. 이 실습은 앞 모듈에서 만든 세션 바인딩 CSRF 토큰을 커스텀 헤더로 보내 두 성질을 함께 얻는다.
- 로그아웃. BFF 세션만 지우면 발급자 쪽 토큰은 만료까지 살아 있다. RFC 7009 폐기 엔드포인트에 먼저 알린다.
현장에서 만나는 모습
BFF 를 붙인 뒤 가장 흔한 사고는 "편의상" 토큰이 다시 새는 것이다. 세션 확인 응답에 access_token 을 같이 실어 주거나, 프런트가 하류 API 를 직접 부르고 싶다며 토큰을 JS 가 읽는 쿠키로 하나 더 내려 준다. 그 순간 BFF 는 이름만 남는다. 반대 방향도 있다. 하류 API 가 "내부망이니까" 토큰 없는 요청을 받아 주면, BFF 를 건너뛴 호출이 그대로 통한다.
한계도 알고 써야 한다. 6.1.4.1절은 악성 스크립트가 사용자의 브라우저를 통해 BFF 로 요청을 보내는 클라이언트 하이재킹은 BFF 로도 막을 수 없다고 적는다. BFF 가 없애는 것은 토큰 탈취이지 XSS 자체가 아니다. 사용자가 탭을 닫으면 공격도 끝난다는 것, 그 차이가 BFF 의 값이다.
다음 실습에서 할 것
한 파드 안에 발급자(8312)·하류 API(8311)·BFF(8310)를 띄우고, 브라우저 역할을 curl 로 해서 로그인 흐름을 밟는다. BFF 가 내려 준 쿠키의 속성을 점검하고, Redis 의 BFF 세션에서 진짜 토큰을 꺼내 브라우저가 본 응답 어디에도 그 값이 없음을 확인한다. 이어서 BFF 경유 호출은 200, 하류 직접 호출은 401 인지, CSRF 토큰 없는 쓰기가 막히는지, 로그아웃 뒤 옛 토큰이 하류에서 죽는지까지 요청으로 증명한다.