refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
+5 -5
View File
@@ -70,7 +70,7 @@
| refresh token | Keycloak | AP1 SPA, AP2 mediator, AP3 BFF 등 해당 소유자 | 새 access token을 얻는 장기 credential |
| server session 식별 cookie | mediator 또는 BFF | 같은 server-side login state의 소유자 | 브라우저 요청을 HttpSession 인증 상태에 연결 |
| proxy session cookie | oauth2-proxy | oauth2-proxy의 auth endpoint | AP4의 minimal client-side session 상태를 다음 auth subrequest에 제시 |
| CSRF token | AP3 BFF | AP3 BFF | cookie가 자동 첨부되는 상태 변경 요청의 의도 확인 |
| CSRF token | AP3 BFF | AP3 BFF | cookie가 자동 첨부되는 상태 변경 요청에 별도 검증 값을 요구해 cross-site forged request를 구분 |
| identity header | oauth2-proxy 결과를 받은 Nginx | AP4 upstream | edge가 확인한 사용자 identity의 투영 |
| internal auth token | AP4 배포 설정 | AP4 upstream | 허용된 edge를 거쳤다는 추가 신뢰 신호 |
@@ -166,7 +166,7 @@ AP1을 보고 나니 refresh token만이라도 브라우저 밖으로 옮기면
AP3를 실행했을 때는 JavaScript 응답에서 OAuth token이 보이지 않았습니다. 처음에는 token을 다루는 일도 함께 사라진 것처럼 보였습니다. 그런데 BFF 코드를 따라가 보니 실제로는 BFF가 session에서 authorized client를 찾고 access token을 붙여 내부 API를 대신 불렀습니다. BFF는 화면에 필요한 API를 브라우저 대신 조합하는 backend입니다.
브라우저는 Bearer header를 만들지 않았지만 session cookie를 요청마다 자동으로 보냈습니다. 그래서 값을 바꾸는 endpoint에는 사용자가 의도한 요청인지 확인할 CSRF token이 필요했습니다. Server 쪽에도 일이 늘었습니다. 재시작 뒤 로그인을 유지할 저장소, 여러 replica가 함께 쓸 session, 저장 token 암호화와 logout을 따로 설계해야 했습니다. 현재 예제는 아직 단일 인스턴스 memory만 사용합니다. 확인을 마치고 보니 `BFF`라는 이름만으로 이런 운영 문제가 해결되는 것은 아니었습니다.
브라우저는 Bearer header를 만들지 않았지만 session cookie를 요청마다 자동으로 보냈습니다. 그래서 값을 바꾸는 endpoint에는 cookie만으로 만들어진 cross-site forged request를 구분하도록 별도 anti-CSRF token을 요구하는 검증이 필요했습니다. Server 쪽에도 일이 늘었습니다. 재시작 뒤 로그인을 유지할 저장소, 여러 replica가 함께 쓸 session, 저장 token 암호화와 logout을 따로 설계해야 했습니다. 현재 예제는 아직 단일 인스턴스 memory만 사용합니다. 확인을 마치고 보니 `BFF`라는 이름만으로 이런 운영 문제가 해결되는 것은 아니었습니다.
### AP4에서 막히는 지점: token 대신 header를 믿는 조건
@@ -180,7 +180,7 @@ AP3를 실행했을 때는 JavaScript 응답에서 OAuth token이 보이지 않
### AP1: OAuth와 JWT 계약을 가장 가까이서 관찰한다
저는 먼저 OAuth의 동작을 브라우저에서 그대로 보고 싶었습니다. 그래서 SPA가 public OAuth client가 되는 AP1부터 만들었습니다. Public client는 브라우저처럼 client secret을 안전하게 숨길애플리케이션입니다. 이 구조에서는 authorization code가 access token으로 바뀌고, 그 token으로 Resource Server를 부르는 과정을 코드와 network에서 함께 볼 수 있었습니다.
저는 먼저 OAuth의 동작을 브라우저에서 그대로 보고 싶었습니다. 그래서 SPA가 public OAuth client가 되는 AP1부터 만들었습니다. AP1 SPA는 브라우저 실행 환경에서 장기 client credential의 기밀성을 유지하고 신뢰할client authentication을 수행하기 어렵기 때문에 public client로 두었습니다. 이 구조에서는 authorization code가 access token으로 바뀌고, 그 token으로 Resource Server를 부르는 과정을 코드와 network에서 함께 볼 수 있었습니다.
`spa-public`에는 Authorization Code와 PKCE S256을 사용했습니다. Implicit flow와 direct access grant는 껐습니다. API도 Keycloak 서명만 확인하고 끝내지 않았습니다. Token을 발급한 issuer와 유효 시간, 이 API를 위해 발급됐다는 `keycloak-pattern-api` audience를 함께 확인했습니다. Realm role은 Spring이 이해하는 `ROLE_` authority로 바꾸었습니다.
@@ -207,7 +207,7 @@ Refresh token만 mediator로 옮기는 AP2나 모든 token을 BFF에 맡기는 A
### AP2: refresh credential은 서버에, 직접 API 호출은 브라우저에 둔다
AP1을 만들고 나니 refresh token까지 JavaScript memory에 있다는 점이 계속 걸렸습니다. 이 장기 credential은 server로 옮기되 브라우저가 Resource Server를 직접 부르는 방식은 남기고 싶었습니다. 그래서 AP2에는 confidential mediator를 두었습니다. Confidential client client secret을 server에서 보관할 수 있는 애플리케이션입니다.
AP1을 만들고 나니 refresh token까지 JavaScript memory에 있다는 점이 계속 걸렸습니다. 이 장기 credential은 server로 옮기되 브라우저가 Resource Server를 직접 부르는 방식은 남기고 싶었습니다. 그래서 AP2에는 confidential mediator를 두었습니다. AP2 mediator는 server에서 client credential을 보호하고 client authentication을 수행할 수 있고, 이 프로젝트에서는 그 인증 방식으로 `client_secret_basic`을 사용했습니다.
Mediator는 Spring `oauth2Login`으로 code를 교환한 뒤 access·refresh token을 server-side authorized-client service에 저장했습니다. 브라우저에는 HttpOnly `AP2_SESSION`을 남기고, API를 부를 때 필요한 현재 access token만 별도 응답으로 주었습니다. 응답 필드는 `access_token`, `token_type`, `expires_at` 세 개로 제한했고 `Cache-Control: no-store``Pragma: no-cache`도 붙였습니다. 브라우저는 이 값을 memory에서 읽어 Bearer header를 만들었습니다.
@@ -1057,7 +1057,7 @@ Controller보다 먼저 Spring CSRF filter가 repository의 expected token과 su
| 다른 port지만 same-site인 요청, header 없음 | session cookie가 실릴 수 있음 | token 부재 거부 | 403 |
| `127.0.0.1`에서 `localhost`로 cross-site POST | SameSite=Lax로`AP3_SESSION`이 request header에서 제외되는지 확인 | 이 test는 이후 server 처리를 고정하지 않음 | 최종 status/body가 아니라 cookie omission이 acceptance point |
SameSite는 cookie의 cross-site 전송을 제한하는 browser 정책이고 CSRF token은 state-changing request의 의도를 server가 검증하는 application protocol입니다. Port가 달라도 site 계산상 같은 경우가 있으므로 둘은 서로 대체할 수 없습니다.
SameSite는 cookie의 cross-site 전송을 제한하는 browser 정책이고 CSRF token은 cookie가 자동 첨부되는 state-changing request에 별도 검증 값을 요구해 cross-site forged request를 구분하는 application protocol입니다. Port가 달라도 site 계산상 같은 경우가 있으므로 둘은 서로 대체할 수 없습니다.
JavaScript가 OAuth token을 받지 않는다고 XSS가 무해해지는 것도 아닙니다. Same-origin 악성 script는 피해자 session으로 BFF endpoint를 호출하고 readable XSRF cookie도 읽을 수 있습니다. AP3가 줄이는 것은 access·refresh token 원문이 browser script에서 유출되어 다른 client나 직접 API 호출에 재사용되는 반경입니다. CSP, output encoding, dependency integrity와 application authorization은 여전히 별도 방어선입니다.