53 lines
5.9 KiB
JSON
53 lines
5.9 KiB
JSON
{
|
|
"kind": "REFERENCE",
|
|
"title": "BFF 인증 구조 설계 기준",
|
|
"slug": "bff-authentication-design-criteria",
|
|
"summary": "BFF가 OAuth token을 server-side에서 관리하고 브라우저는 session cookie로 BFF를 호출할 때 필요한 설계 항목을 정리한다. CSRF 검증, authorized client 저장소, logout, downstream 오류 처리가 핵심이다.",
|
|
"purpose": "BFF 구조에서는 BFF가 authorization code를 token으로 교환하고 access token을 사용해 Resource Server를 호출한다. 따라서 session과 authorized client를 함께 관리하는 보안 구성요소로 본다.\n\ncookie가 credential이 되면 브라우저가 요청마다 자동으로 붙인다. 값을 바꾸는 요청은 사용자의 의도인지 따로 확인해야 한다. 그리고 재시작과 replica 이동을 견딜 저장소도 함께 필요해진다.\n\n여기 있는 것은 「BFF를 쓴다」로 답이 되지 않는 항목들이다.",
|
|
"rules": [
|
|
{
|
|
"title": "브라우저에는 session cookie만 남긴다",
|
|
"body": "access token과 refresh token은 server-side authorized client에 보관한다. 브라우저가 token을 직접 사용할 필요가 없도록 BFF가 downstream 요청의 `Authorization` 헤더를 만든다.\n\nsession cookie는 downstream으로 전달하지 않는다. BFF가 session을 애플리케이션 credential로 소비하고, Resource Server가 아는 Bearer 요청을 새로 만든다. 두 credential은 같은 요청 처리 안에 있지만 검증하는 주체가 다르다."
|
|
},
|
|
{
|
|
"title": "cookie가 credential이면 상태 변경 요청에 CSRF 검증을 둔다",
|
|
"body": "session cookie는 브라우저가 자동으로 전송하므로 상태 변경 endpoint에는 CSRF 검증을 적용한다. 현재 구성은 JavaScript가 CSRF cookie를 읽어 요청 헤더에 같은 값을 전달하는 방식을 사용한다.\n\n노출 값과 제출 값이 다를 수 있다. 응답 본문의 token이 가려진 값이면 헤더에 넣는 값은 cookie에서 읽어야 한다. 두 값을 같다고 가정하고 구현하면 클라이언트가 그대로 403을 받는다.\n\nSameSite와 CSRF token은 역할이 다르다. SameSite는 특정 cross-site 요청에서 cookie 전송을 제한하는 브라우저 정책이고, CSRF token은 cookie가 포함된 상태 변경 요청을 서버가 추가로 검증하는 값이다. 같은 site로 계산되는 다른 origin 요청도 고려해야 한다."
|
|
},
|
|
{
|
|
"title": "session과 authorized client의 수명주기를 따로 설계한다",
|
|
"body": "session은 session ID로 조회하고 authorized client는 registration 이름과 principal name으로 조회한다. shared store를 도입할 때 두 저장 구조를 각각 확인해야 한다.\n\n같은 사용자가 두 브라우저에서 로그인하면 같은 token 항목을 공유하거나 덮어쓴다. session ID마다 token을 따로 보관해야 하면 그렇게 설계해야 한다.\n\n저장소는 재시작과 replica 이동을 견뎌야 한다. 공유 durable store와 session affinity, 저장 token 암호화 중 무엇을 쓸지 정하고 암호화 key 교체 방법도 같이 정한다.\n\nlogout에서는 application session과 authorized client를 모두 정리한다. 두 상태의 lookup key가 다르므로 삭제 처리도 각각 확인해야 한다."
|
|
},
|
|
{
|
|
"title": "downstream 오류를 화면 오류로 바꾸는 규칙을 둔다",
|
|
"body": "Resource Server의 401을 그대로 내려보내면 사용자는 로그인이 끊긴 것인지 권한이 없는 것인지 알 수 없다. timeout과 retry, circuit breaker, 재로그인 전환도 함께 정한다. 모든 UI 요청이 BFF를 지나기 때문에 여기서 정하지 않으면 화면마다 다르게 처리된다."
|
|
},
|
|
{
|
|
"title": "자기 보고 값을 증거로 쓰지 않는다",
|
|
"body": "「브라우저에 token이 없다」고 서버가 응답에 적는 값은 서버가 넣은 상수다. 브라우저를 들여다본 결과가 아니다.\n\n진단 endpoint의 응답과 별개로 브라우저 개발자 도구에서 network 요청과 Web Storage를 직접 확인한다. 애플리케이션이 스스로 보고한 값과 브라우저에서 관측한 결과를 구분해 기록한다."
|
|
},
|
|
{
|
|
"title": "BFF를 넣어도 XSS는 남는다",
|
|
"body": "same-origin에서 악성 script가 실행되면 피해자 session으로 BFF endpoint를 호출하고 JavaScript에서 읽을 수 있는 CSRF cookie에도 접근할 수 있다. BFF는 OAuth token 원문을 브라우저 JavaScript에 전달하지 않지만, CSP와 output encoding, 의존성 무결성, 애플리케이션 인가는 별도로 적용해야 한다."
|
|
}
|
|
],
|
|
"verifiedOn": null,
|
|
"applyWhen": [
|
|
"브라우저가 OAuth token을 받아서는 안 될 때",
|
|
"backend가 화면에 맞춰 여러 API를 조합해야 할 때",
|
|
"로그인 상태를 애플리케이션이 소유해야 할 때",
|
|
"downstream API가 늘어나도 브라우저는 하나만 알게 하고 싶을 때"
|
|
],
|
|
"exceptions": [
|
|
"stateless 직접 API 호출과 독립 client가 핵심이면 BFF를 넣지 않는다. server state와 단일 장애 지점만 늘어난다.",
|
|
"브라우저의 직접 API 호출을 남겨야 하면 refresh credential만 서버로 분리하는 구조가 맞다.",
|
|
"server state를 둘 수 없는 환경이면 브라우저가 token을 직접 다루는 구조가 더 단순하다."
|
|
],
|
|
"examples": [
|
|
"브라우저 요청에는 Authorization 헤더가 없고 session cookie만 있다",
|
|
"BFF가 authorized client에서 access token을 읽어 downstream Bearer 요청을 새로 만든다",
|
|
"CSRF 헤더가 없는 POST는 403이 되고 cookie의 raw 값을 헤더에 넣은 POST는 200이 된다",
|
|
"응답 본문의 token은 가려진 값이고 헤더에 넣는 값은 cookie의 raw 값이다",
|
|
"진단 endpoint의 browserTokenCount는 controller literal이라서 token 비노출의 근거가 아니다"
|
|
]
|
|
}
|