LabHub
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

用 BFF 把令牌留在浏览器之外

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

한 파드 안에 발급자(8312)·하류 API(8311)·BFF(8310)를 세우고, access token 이 브라우저가 볼 수 있는 곳에 한 번도 나타나지 않으며 BFF 를 거친 호출만 하류에서 통한다는 것을 실제 HTTP 요청으로 증명한다.

왜 중요한가

SPA 가 토큰을 직접 쥐면 그 페이지에서 도는 스크립트 하나만 오염돼도 토큰이 밖으로 나가고, 공격자는 만료까지 그 토큰을 쓴다. RFC 10017(OAuth 2.0 for Browser-Based Applications) 6.1절의 BFF 는 토큰을 서버 쪽 세션에만 두고 브라우저에는 HttpOnly 세션 쿠키만 준다. BFF 는 기밀 클라이언트로서 코드를 토큰으로 바꾸고, 하류로 넘길 때 쿠키를 떼고 Bearer 토큰을 붙인다. 이 구조는 한 곳만 느슨해도 무너진다 — 세션 확인 응답에 토큰을 싣거나, 하류가 토큰 없는 요청을 받아 주거나, 로그아웃이 토큰을 폐기하지 않으면 BFF 는 이름만 남는다. 이 실습은 그 세 틈을 각각 요청으로 확인하게 한다.

단계

  1. /root/st/bff/app.py 하나로 발급자(127.0.0.1:8312), 하류 API(127.0.0.1:8311), BFF(127.0.0.1:8310)를 띄운다. 세 곳 모두 GET /health 가 200 과 {"ok":true} 를 준다.
  2. GET /login → 발급자 POST /authorize → BFF /callback 순서로 로그인하고, 콜백이 내려 준 Set-Cookie 한 줄을 /root/st/bff/cookie.txt 에 저장한다. 쿠키는 __Host-Http-bff 이름에 HttpOnly, Secure, SameSite=Strict, Path=/ 를 갖는다.
  3. Redis 의 bff:sess:<세션ID>access_token 이 있고 그 값이 브라우저가 받은 응답의 헤더·본문 어디에도 없는지 /root/st/bff/exposure.txt 에 적는다.
  4. bob 으로 로그인한 쿠키로 GET /bff/api/me 를 불러 BFF 가 Bearer 를 붙여 하류에서 200 과 sub=bob 을 받고, 하류에 쿠키가 넘어가지 않는지 /root/st/bff/proxy.txt 에 적는다.
  5. 하류 /me 를 토큰 없이, 세션 쿠키만, 엉터리 Bearer 로 직접 불러 모두 401 과 WWW-Authenticate: Bearer 인지 /root/st/bff/direct.txt 에 적는다.
  6. POST /bff/api/notes 가 올바른 X-CSRF-Token 일 때만 201 이고 없거나 틀리면 403 인지 /root/st/bff/csrf.txt 에 적는다.
  7. curl -X POST 로 로그아웃한 뒤 BFF 세션이 사라지고 옛 access token 이 하류에서 401 인지 /root/st/bff/logout.txt 에 적는다.
  8. /root/st/bff/e2e.sh 로 전체 흐름을 시험해 /root/st/bff/e2e.out 에 여섯 줄을 남긴다.

참고

발급자·하류 API·BFF 띄우기

/root/st/bff/app.py 하나로 발급자(127.0.0.1:8312), 하류 API(127.0.0.1:8311), BFF(127.0.0.1:8310)를 띄운다. 세 곳 모두 GET /health 가 200 과 {"ok":true} 를 준다.

한 프로세스 안에서 ThreadingHTTPServer 셋을 스레드로 돌리면 띄우고 내리기가 한 번에 끝납니다. 서버는 백그라운드로 띄우고 세 포트가 모두 응답할 때까지 폴링하세요.

브라우저가 받는 것은 세션 쿠키 하나

GET /login → 발급자 POST /authorize(폼 user=alice&pw=wonderland) → BFF /callback 순서로 로그인하고, 콜백이 내려 준 Set-Cookie 한 줄을 /root/st/bff/cookie.txt 에 저장한다. 쿠키 이름은 __Host-Http-bff 이고 HttpOnly, Secure, SameSite=Strict, Path=/ 를 가지며 Domain 은 없다.

curl -w '%{redirect_url}' 로 302 의 Location 을 받아 다음 요청에 넘기면 브라우저처럼 흐름을 밟을 수 있습니다. 발급자에 계정을 보내는 요청은 POST 입니다. RFC 10017 6.1.3.2 가 HttpOnly 와 Secure 를 MUST 로 둡니다.

토큰은 BFF 세션에만 있다

로그인 뒤 Redis 의 bff:sess:<세션ID>(JSON)에 access_token 이 있고, 그 값이 콜백 응답과 GET /bff/session 응답의 헤더·본문 어디에도 없는지 대조해 /root/st/bff/exposure.txttoken_in_store=yes, token_in_body=no, token_in_cookie=no 로 적는다. GET /bff/sessionauth·user·csrf 만 준다.

세션 ID 는 쿠키 병에서, 진짜 토큰은 redis-cli GET 으로 꺼냅니다. 그 문자열을 grep -F 로 응답 파일들에서 찾아보세요. 세션 확인 응답에 편의상 토큰을 싣는 순간 BFF 는 이름만 남습니다.

BFF 가 Bearer 를 붙여 하류를 부른다

bob/builder 로 로그인한 쿠키로 GET /bff/api/me 를 부르면 BFF 가 쿠키를 떼고 Authorization: Bearer <토큰> 을 붙여 하류 GET /me 로 넘겨 200 과 {"sub":"bob"} 을 받는다. 하류 GET /inspectcookie_forwarded 로 쿠키가 넘어왔는지 알려 준다. 결과를 /root/st/bff/proxy.txtme=200, sub=bob, cookie_forwarded=False 로 적는다.

BFF 는 /bff/api/ 뒤의 경로를 하류로 옮기고, 요청 헤더를 통째로 복사하지 말고 Authorization 을 새로 만드세요. 하류가 토큰 없는 요청을 401 로 막아야 BFF 경유 200 이 곧 BFF 가 토큰을 붙였다는 증거가 됩니다.

하류를 직접 부르면 401

BFF 를 건너뛰고 하류 GET http://127.0.0.1:8311/me 를 토큰 없이, BFF 세션 쿠키만 들고, 엉터리 Bearer 값으로 각각 불러 모두 401 이고 WWW-Authenticate: Bearer 가 붙는지 /root/st/bff/direct.txtno_token=401, cookie_only=401, bogus_bearer=401, www_authenticate=Bearer 로 적는다. 토큰 없는 POST /notes 도 401 이어야 한다.

하류는 쿠키를 보지 않고 Authorization 헤더만 봅니다. 토큰이 살아 있는지는 발급자의 introspection 에 물어 판단하세요. RFC 6750 3절이 401 응답에 붙일 헤더를 정합니다.

BFF 의 쓰기는 CSRF 토큰으로 지킨다

GET /bff/sessioncsrf 값을 X-CSRF-Token 헤더로 보낼 때만 POST /bff/api/notes(폼 text=...)가 하류로 넘어가 201 이 되고, 헤더가 없거나 틀리면 403 이며 하류에 기록되지 않는지 /root/st/bff/csrf.txtno_header=403, wrong=403, right=201 로 적는다.

브라우저는 쿠키를 자동으로 싣기 때문에 쿠키만으로는 위조 요청과 구별이 안 됩니다. 세션에 CSRF 토큰을 함께 넣어 두고 hmac.compare_digest 로 비교하세요. 거절은 하류를 부르기 전에 해야 합니다.

로그아웃은 세션과 토큰을 함께 끝낸다

로그인 뒤 curl -X POSTPOST /logout(쿠키와 X-CSRF-Token 포함)을 부르고, 같은 쿠키의 GET /bff/sessionauth False, Redis 의 bff:sess:<세션ID> 가 사라짐, 로그아웃 전에 꺼내 둔 access token 으로 하류 /me 를 직접 부르면 401 인지 /root/st/bff/logout.txtsession_auth=False, store_key_gone=yes, old_token_direct=401 로 적는다.

세션 키만 지우면 발급자 쪽 토큰은 만료까지 살아 있습니다. 로그아웃에서 BFF 가 발급자의 폐기 엔드포인트(RFC 7009)를 먼저 부르세요. -X POST 를 빼면 curl 은 GET 을 보냅니다.

전체 흐름 한 번에 증명

/root/st/bff/e2e.sh 로 로그인부터 로그아웃까지 시험해 /root/st/bff/e2e.outcookie_httponly=yes, token_exposed=no, via_bff=200, direct_no_token=401, csrf_missing=403, after_logout_token=401 여섯 줄을 남긴다. 채점기는 e2e.sh 를 다시 돌려 같은 결과가 나오는지도 본다.

앞 스텝들을 한 스크립트로 이어 붙입니다. 스크립트는 자기가 만든 임시 파일을 지우고, 결과는 표준 출력으로만 내면 다시 돌려도 안전합니다.