455 lines
37 KiB
YAML
455 lines
37 KiB
YAML
- generic [ref=f28e3]:
|
|
- link "본문으로 건너뛰기" [ref=f28e4] [cursor=pointer]:
|
|
- /url: "#main-content"
|
|
- banner [ref=f28e5]:
|
|
- generic [ref=f28e6]:
|
|
- link "TechLog Studio" [ref=f28e7] [cursor=pointer]:
|
|
- /url: /studio
|
|
- text: TechLog
|
|
- generic [ref=f28e8]: Studio
|
|
- navigation "Studio 주 탐색" [ref=f28e10]:
|
|
- link "작업본" [ref=f28e11] [cursor=pointer]:
|
|
- /url: /studio/documents
|
|
- link "게시 기록" [ref=f28e12] [cursor=pointer]:
|
|
- /url: /studio/publications
|
|
- link "새 문서" [ref=f28e13] [cursor=pointer]:
|
|
- /url: /studio/documents/new
|
|
- link "주제·프로젝트" [ref=f28e14] [cursor=pointer]:
|
|
- /url: /studio/taxonomy
|
|
- link "릴리즈" [ref=f28e15] [cursor=pointer]:
|
|
- /url: /studio/releases
|
|
- link "공개 사이트 보기" [ref=f28e16] [cursor=pointer]:
|
|
- /url: /
|
|
- button "로그아웃" [ref=f28e17]
|
|
- main [ref=f28e18]:
|
|
- generic [ref=f28e19]:
|
|
- generic [ref=f28e20]:
|
|
- region [ref=f28e21]:
|
|
- generic [ref=f28e22]:
|
|
- paragraph [ref=f28e23]: REFERENCE · VERSION 34
|
|
- heading "문서 편집" [level=1] [ref=f28e24]
|
|
- paragraph [ref=f28e25]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
|
|
- region [ref=f28e26]:
|
|
- generic [ref=f28e27]:
|
|
- paragraph [ref=f28e28]: DOCUMENT
|
|
- heading "기본 정보" [level=2] [ref=f28e29]
|
|
- generic [ref=f28e30]:
|
|
- generic [ref=f28e31]:
|
|
- generic [ref=f28e32]: 제목
|
|
- textbox "제목" [ref=f28e33]: Authorization Code Flow의 Endpoint와 Credential 이동 기준
|
|
- generic [ref=f28e34]:
|
|
- generic [ref=f28e35]: slug
|
|
- textbox "slug" [ref=f28e36]:
|
|
- /placeholder: 비우면 제목에서 만듭니다 (영문 소문자·숫자·하이픈)
|
|
- text: authorization-code-endpoint-credential-movement
|
|
- generic [ref=f28e37]:
|
|
- generic [ref=f28e38]: 요약
|
|
- textbox "요약" [ref=f28e39]: Authorization Endpoint에서 Redirect, Token Endpoint, JWK 검증, Resource API까지 각 지점에서 무엇이 이동하고 무엇이 이동하지 않는지 확인한다. 먼저 client_secret이 가는 곳과 가지 않는 곳을 나눠 보자.
|
|
- generic [ref=f28e40]:
|
|
- generic [ref=f28e41]: Topic
|
|
- combobox "Topic" [ref=f28e42]:
|
|
- option "선택하지 않음"
|
|
- option "OAuth/OIDC 인증 경계" [selected]
|
|
- generic [ref=f28e43]:
|
|
- generic [ref=f28e44]: Project
|
|
- combobox "Project" [ref=f28e45]:
|
|
- option "미지정"
|
|
- option "Backend Clean Architecture"
|
|
- option "KeyCloak Patterns" [selected]
|
|
- option "Liner N + 1문제"
|
|
- group "관계" [ref=f28e46]:
|
|
- generic [ref=f28e48]:
|
|
- generic [ref=f28e49]:
|
|
- generic [ref=f28e50]: 관계 1 대상
|
|
- combobox "관계 1 대상" [ref=f28e51]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준"
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
|
|
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
|
|
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
|
|
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
|
|
- option "외부 IdP Federation과 Application 인증 경계"
|
|
- option "OAuth/OIDC 인증 패턴 선택 기준"
|
|
- option "OAuth Token과 Application Session을 구분하는 기준"
|
|
- option "Public Client와 Confidential Client 구분 기준" [disabled]
|
|
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
|
|
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
|
|
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [selected]
|
|
- generic [ref=f28e52]:
|
|
- generic [ref=f28e53]: 관계 1 이유
|
|
- textbox "관계 1 이유" [ref=f28e54]: 브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다.
|
|
- generic [ref=f28e55]:
|
|
- button "위로" [disabled] [ref=f28e56]
|
|
- button "아래로" [ref=f28e57]
|
|
- button "삭제" [ref=f28e58]
|
|
- generic [ref=f28e59]:
|
|
- generic [ref=f28e60]:
|
|
- generic [ref=f28e61]: 관계 2 대상
|
|
- combobox "관계 2 대상" [ref=f28e62]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준"
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
|
|
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
|
|
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
|
|
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
|
|
- option "외부 IdP Federation과 Application 인증 경계"
|
|
- option "OAuth/OIDC 인증 패턴 선택 기준"
|
|
- option "OAuth Token과 Application Session을 구분하는 기준"
|
|
- option "Public Client와 Confidential Client 구분 기준" [disabled]
|
|
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [selected]
|
|
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
|
|
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
|
|
- generic [ref=f28e63]:
|
|
- generic [ref=f28e64]: 관계 2 이유
|
|
- textbox "관계 2 이유" [ref=f28e65]: confidential client가 token endpoint에서 자기 client를 인증하는 실례다.
|
|
- generic [ref=f28e66]:
|
|
- button "위로" [ref=f28e67]
|
|
- button "아래로" [ref=f28e68]
|
|
- button "삭제" [ref=f28e69]
|
|
- generic [ref=f28e70]:
|
|
- generic [ref=f28e71]:
|
|
- generic [ref=f28e72]: 관계 3 대상
|
|
- combobox "관계 3 대상" [ref=f28e73]:
|
|
- option "대상 선택"
|
|
- option "인증 구조를 보안 성숙도 단계로 취급하지 않는다"
|
|
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
|
|
- option "Authorization Code Flow의 Endpoint와 Credential 이동 기준"
|
|
- option "BFF 인증 구조 설계 기준"
|
|
- option "BFF가 OAuth Token을 관리하는 조건"
|
|
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가"
|
|
- option "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정"
|
|
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
|
|
- option "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유"
|
|
- option "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건"
|
|
- option "외부 IdP Federation을 별도의 인증 구조로 세지 않는다"
|
|
- option "외부 IdP Federation과 Application 인증 경계"
|
|
- option "OAuth/OIDC 인증 패턴 선택 기준"
|
|
- option "OAuth Token과 Application Session을 구분하는 기준"
|
|
- option "Public Client와 Confidential Client 구분 기준" [selected]
|
|
- option "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [disabled]
|
|
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
|
|
- option "SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [disabled]
|
|
- generic [ref=f28e74]:
|
|
- generic [ref=f28e75]: 관계 3 이유
|
|
- textbox "관계 3 이유" [ref=f28e76]: client 종류가 정해져야 PKCE와 client 인증의 자리가 정해진다.
|
|
- generic [ref=f28e77]:
|
|
- button "위로" [ref=f28e78]
|
|
- button "아래로" [disabled] [ref=f28e79]
|
|
- button "삭제" [ref=f28e80]
|
|
- button "관계 추가" [ref=f28e81]
|
|
- region [ref=f28e82]:
|
|
- generic [ref=f28e83]:
|
|
- paragraph [ref=f28e84]: REFERENCE
|
|
- heading "재사용할 기준" [level=2] [ref=f28e85]
|
|
- generic [ref=f28e86]:
|
|
- generic [ref=f28e87]: 목적
|
|
- textbox "목적" [ref=f28e88]: "Authorization Endpoint와 Token Endpoint는 역할과 호출 방식이 다르다. 이 구분을 해야 SPA에서 client_secret이 어디로 갔는지, PKCE가 어느 구간을 지키는지 이해하기 쉽다. 하나는 브라우저의 full-page navigation이고 하나는 server-to-server 호출이 될 수도 있고 browser-to-server 호출이 될 수도 있다. 노출되는 것도, 인증하는 방법도 다르다. Authorization Endpoint 경로 : 브라우저 주소창 남는 곳 : 히스토리·서버 로그·referrer client 인증 : x Token Endpoint 경로 : body와 Authorization 헤더 보내는 쪽 : client 종류에 따라 server 또는 브라우저 client 인증 : o"
|
|
- group "규칙" [ref=f28e89]:
|
|
- generic [ref=f28e91]:
|
|
- generic [ref=f28e92]:
|
|
- generic [ref=f28e93]: 규칙 1 제목
|
|
- textbox "규칙 1 제목" [ref=f28e94]: Authorization Endpoint에는 client_secret을 보내지 않는다
|
|
- generic [ref=f28e95]:
|
|
- generic [ref=f28e96]: 규칙 1 본문
|
|
- textbox "규칙 1 본문" [ref=f28e97]: 이 요청은 브라우저 주소창을 통해 나간다. 그래서 URL이 주소창에도, 브라우저 히스토리에도, 서버 접근 로그에도, 그리고 링크를 타고 온 경우 referrer에도 남는다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. secret이 필요한 인증은 아직 하지 않는다. 반대로 말하면 이 목록에 없는 값을 여기 넣으면 그 값도 같은 곳에 다 남는다.
|
|
- generic [ref=f28e98]:
|
|
- button "위로" [disabled] [ref=f28e99]
|
|
- button "아래로" [ref=f28e100]
|
|
- button "삭제" [ref=f28e101]
|
|
- generic [ref=f28e102]:
|
|
- generic [ref=f28e103]:
|
|
- generic [ref=f28e104]: 규칙 2 제목
|
|
- textbox "규칙 2 제목" [ref=f28e105]: Token Endpoint에서 비로소 client를 인증한다
|
|
- generic [ref=f28e106]:
|
|
- generic [ref=f28e107]: 규칙 2 본문
|
|
- textbox "규칙 2 본문" [ref=f28e108]: code를 access token으로 바꾸는 요청은 credential을 URL query가 아니라 body와 Authorization 헤더에 싣는다. 그래서 client 인증을 여기서 한다. confidential client는 client_secret_basic처럼 secret을 함께 보낸다. 주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 debug 로그, reverse proxy 로그, tracing과 APM, packet capture에 남을 수 있어서 credential masking을 따로 둔다. 이 요청을 누가 보내는지는 client 종류에 따라 갈린다. server가 보내면 server-to-server이고, secret이 없는 SPA가 보내면 브라우저가 직접 보낸다. token endpoint를 server 안에서만 부르게 하려면 client 종류부터 confidential로 정해야 한다.
|
|
- generic [ref=f28e109]:
|
|
- button "위로" [ref=f28e110]
|
|
- button "아래로" [ref=f28e111]
|
|
- button "삭제" [ref=f28e112]
|
|
- generic [ref=f28e113]:
|
|
- generic [ref=f28e114]:
|
|
- generic [ref=f28e115]: 규칙 3 제목
|
|
- textbox "규칙 3 제목" [ref=f28e116]: PKCE는 두 요청을 같은 주체에 묶는다
|
|
- generic [ref=f28e117]:
|
|
- generic [ref=f28e118]: 규칙 3 본문
|
|
- textbox "규칙 3 본문" [ref=f28e119]: 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서 이 두개가 일치하는지 확인 한다. 이 2개가 일치해야 토큰 교환이 되게 된다. code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
|
|
- generic [ref=f28e120]:
|
|
- button "위로" [ref=f28e121]
|
|
- button "아래로" [ref=f28e122]
|
|
- button "삭제" [ref=f28e123]
|
|
- generic [ref=f28e124]:
|
|
- generic [ref=f28e125]:
|
|
- generic [ref=f28e126]: 규칙 4 제목
|
|
- textbox "규칙 4 제목" [ref=f28e127]: issuer 검증값과 JWK 조회 주소를 같은 값으로 맞추려 하지 않는다
|
|
- generic [ref=f28e128]:
|
|
- generic [ref=f28e129]: 규칙 4 본문
|
|
- textbox "규칙 4 본문" [ref=f28e130]: issuer는 요청을 보내는 주소가 아니라 token의 canonical issuer identifier다. 검증은 발급된 token의 iss claim이 그 값과 같은지를 본다. JWK 조회 주소는 실제로 공개키를 가져오는 network 경로다. 이 예제에서는 브라우저가 보는 주소와 컨테이너 안에서 닿는 주소가 다르다. 컨테이너 안에서는 자기 localhost가 그 서버가 아니므로 service 이름을 써야 하고, 브라우저는 그 이름에 닿지 못한다. issuer 검증값과 endpoint 연결 주소는 따로 구성한다. 둘을 하나로 맞추려 하면 로그인 redirect가 깨지거나 서버가 키를 못 가져온다.
|
|
- generic [ref=f28e131]:
|
|
- button "위로" [ref=f28e132]
|
|
- button "아래로" [ref=f28e133]
|
|
- button "삭제" [ref=f28e134]
|
|
- generic [ref=f28e135]:
|
|
- generic [ref=f28e136]:
|
|
- generic [ref=f28e137]: 규칙 5 제목
|
|
- textbox "규칙 5 제목" [ref=f28e138]: Resource API는 서명만 보고 끝내지 않는다
|
|
- generic [ref=f28e139]:
|
|
- generic [ref=f28e140]: 규칙 5 본문
|
|
- textbox "규칙 5 본문" [ref=f28e141]: 서명이 맞다는 것은 그 IdP가 발급했다는 뜻일 뿐이다. 같은 IdP가 다른 API용으로 발급한 token도 서명은 맞다. 그래서 issuer와 유효 시간, 그리고 이 API를 위해 발급됐다는 audience를 함께 본다. audience 검증이 빠지면 옆 서비스의 token으로 우리 API가 열린다.
|
|
- generic [ref=f28e142]:
|
|
- button "위로" [ref=f28e143]
|
|
- button "아래로" [ref=f28e144]
|
|
- button "삭제" [ref=f28e145]
|
|
- generic [ref=f28e146]:
|
|
- generic [ref=f28e147]:
|
|
- generic [ref=f28e148]: 규칙 6 제목
|
|
- textbox "규칙 6 제목" [ref=f28e149]: redirect_uri는 exact match로 좁힌다
|
|
- generic [ref=f28e150]:
|
|
- generic [ref=f28e151]: 규칙 6 본문
|
|
- textbox "규칙 6 본문" [ref=f28e152]: wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른 경로로도 code가 갈 수 있다. 실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다. 등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
|
|
- generic [ref=f28e153]:
|
|
- button "위로" [ref=f28e154]
|
|
- button "아래로" [ref=f28e155]
|
|
- button "삭제" [ref=f28e156]
|
|
- generic [ref=f28e157]:
|
|
- generic [ref=f28e158]:
|
|
- generic [ref=f28e159]: 규칙 7 제목
|
|
- textbox "규칙 7 제목" [ref=f28e160]: 로그인 구간과 API 호출 구간을 한 줄로 그리지 않는다
|
|
- generic [ref=f28e161]:
|
|
- generic [ref=f28e162]: 규칙 7 본문
|
|
- textbox "규칙 7 본문" [ref=f28e163]: 로그인 구간은 authorization request에서 시작해 callback과 code 교환을 지나 로그인 상태를 만드는 데까지다. API 호출 구간은 브라우저 입력, 중간 계층의 credential 변환, 보호 자원의 검증, 최종 응답이다. 두 구간을 한 줄로 이어 그리면 누가 code를 바꾸고 누가 API를 부르는지가 겹쳐 보인다. 구조가 갈리는 자리가 바로 여기라서 나눠서 그려야 한다.
|
|
- generic [ref=f28e164]:
|
|
- button "위로" [ref=f28e165]
|
|
- button "아래로" [disabled] [ref=f28e166]
|
|
- button "삭제" [ref=f28e167]
|
|
- button "규칙 추가" [ref=f28e168]
|
|
- group "적용 조건" [ref=f28e169]:
|
|
- generic [ref=f28e171]:
|
|
- generic [ref=f28e172]:
|
|
- generic [ref=f28e173]: 적용 조건 1
|
|
- textbox "적용 조건 1" [ref=f28e174]: Authorization Code Flow를 쓰는 client를 설정하거나 문서로 설명할 때
|
|
- generic [ref=f28e175]:
|
|
- button "위로" [disabled] [ref=f28e176]
|
|
- button "아래로" [ref=f28e177]
|
|
- button "삭제" [ref=f28e178]
|
|
- generic [ref=f28e179]:
|
|
- generic [ref=f28e180]:
|
|
- generic [ref=f28e181]: 적용 조건 2
|
|
- textbox "적용 조건 2" [ref=f28e182]: 브라우저 요청과 server-to-server 요청이 한 흐름에 섞여 있을 때
|
|
- generic [ref=f28e183]:
|
|
- button "위로" [ref=f28e184]
|
|
- button "아래로" [ref=f28e185]
|
|
- button "삭제" [ref=f28e186]
|
|
- generic [ref=f28e187]:
|
|
- generic [ref=f28e188]:
|
|
- generic [ref=f28e189]: 적용 조건 3
|
|
- textbox "적용 조건 3" [ref=f28e190]: endpoint별로 무엇이 노출되는지 나눠야 할 때
|
|
- generic [ref=f28e191]:
|
|
- button "위로" [ref=f28e192]
|
|
- button "아래로" [ref=f28e193]
|
|
- button "삭제" [ref=f28e194]
|
|
- generic [ref=f28e195]:
|
|
- generic [ref=f28e196]:
|
|
- generic [ref=f28e197]: 적용 조건 4
|
|
- textbox "적용 조건 4" [ref=f28e198]: PKCE와 client 인증의 자리를 정할 때
|
|
- generic [ref=f28e199]:
|
|
- button "위로" [ref=f28e200]
|
|
- button "아래로" [disabled] [ref=f28e201]
|
|
- button "삭제" [ref=f28e202]
|
|
- button "적용 조건 추가" [ref=f28e203]
|
|
- group "예외" [ref=f28e204]:
|
|
- generic [ref=f28e206]:
|
|
- generic [ref=f28e207]:
|
|
- generic [ref=f28e208]: 예외 1
|
|
- textbox "예외 1" [ref=f28e209]: Client Credentials처럼 사용자 없이 token을 받는 흐름은 Authorization Endpoint를 지나지 않는다.
|
|
- generic [ref=f28e210]:
|
|
- button "위로" [disabled] [ref=f28e211]
|
|
- button "아래로" [ref=f28e212]
|
|
- button "삭제" [ref=f28e213]
|
|
- generic [ref=f28e214]:
|
|
- generic [ref=f28e215]:
|
|
- generic [ref=f28e216]: 예외 2
|
|
- textbox "예외 2" [ref=f28e217]: Device Authorization Grant는 브라우저 redirect 대신 별도의 사용자 code 단계를 쓴다. redirect_uri 항목이 그대로 적용되지 않는다.
|
|
- generic [ref=f28e218]:
|
|
- button "위로" [ref=f28e219]
|
|
- button "아래로" [disabled] [ref=f28e220]
|
|
- button "삭제" [ref=f28e221]
|
|
- button "예외 추가" [ref=f28e222]
|
|
- group "예시" [ref=f28e223]:
|
|
- generic [ref=f28e225]:
|
|
- generic [ref=f28e226]:
|
|
- generic [ref=f28e227]: 예시 1
|
|
- textbox "예시 1" [ref=f28e228]: authorization request에는 code_challenge_method=S256이 있고 client secret은 없다
|
|
- generic [ref=f28e229]:
|
|
- button "위로" [disabled] [ref=f28e230]
|
|
- button "아래로" [ref=f28e231]
|
|
- button "삭제" [ref=f28e232]
|
|
- generic [ref=f28e233]:
|
|
- generic [ref=f28e234]:
|
|
- generic [ref=f28e235]: 예시 2
|
|
- textbox "예시 2" [ref=f28e236]: token request에는 code_verifier가 있다. secret을 가진 client는 이 요청에서 자기를 인증한다
|
|
- generic [ref=f28e237]:
|
|
- button "위로" [ref=f28e238]
|
|
- button "아래로" [ref=f28e239]
|
|
- button "삭제" [ref=f28e240]
|
|
- generic [ref=f28e241]:
|
|
- generic [ref=f28e242]:
|
|
- generic [ref=f28e243]: 예시 3
|
|
- textbox "예시 3" [ref=f28e244]: expected issuer는 http://localhost:8080/realms/keycloak-patterns 이고 JWK 조회는 컨테이너 network 주소를 쓴다
|
|
- generic [ref=f28e245]:
|
|
- button "위로" [ref=f28e246]
|
|
- button "아래로" [ref=f28e247]
|
|
- button "삭제" [ref=f28e248]
|
|
- generic [ref=f28e249]:
|
|
- generic [ref=f28e250]:
|
|
- generic [ref=f28e251]: 예시 4
|
|
- textbox "예시 4" [ref=f28e252]: audience에 keycloak-pattern-api 가 없으면 invalid_token 결과가 되어 401이 된다
|
|
- generic [ref=f28e253]:
|
|
- button "위로" [ref=f28e254]
|
|
- button "아래로" [ref=f28e255]
|
|
- button "삭제" [ref=f28e256]
|
|
- generic [ref=f28e257]:
|
|
- generic [ref=f28e258]:
|
|
- generic [ref=f28e259]: 예시 5
|
|
- textbox "예시 5" [ref=f28e260]: redirect allowlist에 wildcard가 있으면 등록한 host의 다른 경로로도 code가 갈 수 있다
|
|
- generic [ref=f28e261]:
|
|
- button "위로" [ref=f28e262]
|
|
- button "아래로" [disabled] [ref=f28e263]
|
|
- button "삭제" [ref=f28e264]
|
|
- button "예시 추가" [ref=f28e265]
|
|
- generic [ref=f28e266]:
|
|
- generic [ref=f28e267]: 마지막 검증일
|
|
- textbox "마지막 검증일" [ref=f28e268]: 2026-08-25
|
|
- region [ref=f28e269]:
|
|
- generic [ref=f28e270]:
|
|
- paragraph [ref=f28e271]: LIVE
|
|
- heading "즉시 미리보기" [level=2] [ref=f28e272]
|
|
- generic [ref=f28e275]:
|
|
- generic [ref=f28e276]:
|
|
- navigation "문서 경로" [ref=f28e277]:
|
|
- link "Reference" [ref=f28e278] [cursor=pointer]:
|
|
- /url: /explore/references
|
|
- generic [ref=f28e279]: /
|
|
- generic [ref=f28e280]: OAuth/OIDC 인증 경계
|
|
- generic [ref=f28e281]: /
|
|
- link "KeyCloak Patterns" [ref=f28e282] [cursor=pointer]:
|
|
- /url: /projects/keycloak-patterns
|
|
- heading "Authorization Code Flow의 Endpoint와 Credential 이동 기준" [level=1] [ref=f28e283]
|
|
- paragraph [ref=f28e284]: Authorization Endpoint에서 Redirect, Token Endpoint, JWK 검증, Resource API까지 각 지점에서 무엇이 이동하고 무엇이 이동하지 않는지 확인한다. 먼저 client_secret이 가는 곳과 가지 않는 곳을 나눠 보자.
|
|
- generic [ref=f28e285]:
|
|
- generic [ref=f28e286]:
|
|
- term [ref=f28e287]: 유형
|
|
- definition [ref=f28e288]: Reference
|
|
- generic [ref=f28e289]:
|
|
- term [ref=f28e290]: 프로젝트
|
|
- definition [ref=f28e291]: KeyCloak Patterns
|
|
- generic [ref=f28e292]:
|
|
- term [ref=f28e293]: 게시
|
|
- definition [ref=f28e294]: 2026.08.25
|
|
- region [ref=f28e295]:
|
|
- paragraph [ref=f28e296]: Purpose
|
|
- heading "이 기준을 쓰는 이유" [level=2] [ref=f28e297]
|
|
- paragraph [ref=f28e298]: "Authorization Endpoint와 Token Endpoint는 역할과 호출 방식이 다르다.이 구분을 해야 SPA에서 client_secret이 어디로 갔는지, PKCE가 어느 구간을 지키는지 이해하기 쉽다.하나는 브라우저의 full-page navigation이고 하나는 server-to-server 호출이 될 수도 있고 browser-to-server 호출이 될 수도 있다.노출되는 것도, 인증하는 방법도 다르다.Authorization Endpoint경로 : 브라우저 주소창 남는 곳 : 히스토리·서버 로그·referrer client 인증 : xToken Endpoint경로 : body와 Authorization 헤더 보내는 쪽 : client 종류에 따라 server 또는 브라우저 client 인증 : o"
|
|
- article [ref=f28e299]:
|
|
- region [ref=f28e300]:
|
|
- heading "판단 기준" [level=2] [ref=f28e301]
|
|
- list [ref=f28e302]:
|
|
- listitem [ref=f28e303]:
|
|
- generic [ref=f28e304]: "01"
|
|
- generic [ref=f28e305]:
|
|
- heading "Authorization Endpoint에는 client_secret을 보내지 않는다" [level=3] [ref=f28e306]
|
|
- paragraph [ref=f28e307]: 이 요청은 브라우저 주소창을 통해 나간다. 그래서 URL이 주소창에도, 브라우저 히스토리에도, 서버 접근 로그에도, 그리고 링크를 타고 온 경우 referrer에도 남는다. 여기 실리는 값은 client_id, redirect_uri, response_type, scope, state, code_challenge, code_challenge_method다. secret이 필요한 인증은 아직 하지 않는다. 반대로 말하면 이 목록에 없는 값을 여기 넣으면 그 값도 같은 곳에 다 남는다.
|
|
- listitem [ref=f28e308]:
|
|
- generic [ref=f28e309]: "02"
|
|
- generic [ref=f28e310]:
|
|
- heading "Token Endpoint에서 비로소 client를 인증한다" [level=3] [ref=f28e311]
|
|
- paragraph [ref=f28e312]: code를 access token으로 바꾸는 요청은 credential을 URL query가 아니라 body와 Authorization 헤더에 싣는다. 그래서 client 인증을 여기서 한다. confidential client는 client_secret_basic처럼 secret을 함께 보낸다. 주소창과 히스토리에 남지 않는다는 뜻이지 어디에도 기록되지 않는다는 뜻은 아니다. 애플리케이션 debug 로그, reverse proxy 로그, tracing과 APM, packet capture에 남을 수 있어서 credential masking을 따로 둔다. 이 요청을 누가 보내는지는 client 종류에 따라 갈린다. server가 보내면 server-to-server이고, secret이 없는 SPA가 보내면 브라우저가 직접 보낸다. token endpoint를 server 안에서만 부르게 하려면 client 종류부터 confidential로 정해야 한다.
|
|
- listitem [ref=f28e313]:
|
|
- generic [ref=f28e314]: "03"
|
|
- generic [ref=f28e315]:
|
|
- heading "PKCE는 두 요청을 같은 주체에 묶는다" [level=3] [ref=f28e316]
|
|
- paragraph [ref=f28e317]: 처음 요청에 code_challenge를 담아서 보내고, 교환할 때 원본인 code_verifier를 보내서 이 두개가 일치하는지 확인 한다. 이 2개가 일치해야 토큰 교환이 되게 된다. code를 누가 훔쳐 가도 verifier가 없으면 token으로 바꾸지 못한다.
|
|
- listitem [ref=f28e318]:
|
|
- generic [ref=f28e319]: "04"
|
|
- generic [ref=f28e320]:
|
|
- heading "issuer 검증값과 JWK 조회 주소를 같은 값으로 맞추려 하지 않는다" [level=3] [ref=f28e321]
|
|
- paragraph [ref=f28e322]: issuer는 요청을 보내는 주소가 아니라 token의 canonical issuer identifier다. 검증은 발급된 token의 iss claim이 그 값과 같은지를 본다. JWK 조회 주소는 실제로 공개키를 가져오는 network 경로다. 이 예제에서는 브라우저가 보는 주소와 컨테이너 안에서 닿는 주소가 다르다. 컨테이너 안에서는 자기 localhost가 그 서버가 아니므로 service 이름을 써야 하고, 브라우저는 그 이름에 닿지 못한다. issuer 검증값과 endpoint 연결 주소는 따로 구성한다. 둘을 하나로 맞추려 하면 로그인 redirect가 깨지거나 서버가 키를 못 가져온다.
|
|
- listitem [ref=f28e323]:
|
|
- generic [ref=f28e324]: "05"
|
|
- generic [ref=f28e325]:
|
|
- heading "Resource API는 서명만 보고 끝내지 않는다" [level=3] [ref=f28e326]
|
|
- paragraph [ref=f28e327]: 서명이 맞다는 것은 그 IdP가 발급했다는 뜻일 뿐이다. 같은 IdP가 다른 API용으로 발급한 token도 서명은 맞다. 그래서 issuer와 유효 시간, 그리고 이 API를 위해 발급됐다는 audience를 함께 본다. audience 검증이 빠지면 옆 서비스의 token으로 우리 API가 열린다.
|
|
- listitem [ref=f28e328]:
|
|
- generic [ref=f28e329]: "06"
|
|
- generic [ref=f28e330]:
|
|
- heading "redirect_uri는 exact match로 좁힌다" [level=3] [ref=f28e331]
|
|
- paragraph [ref=f28e332]: wildcard allowlist는 학습 환경에서 편하다. 다만 허용 범위가 넓으면 같은 호스트의 다른 경로로도 code가 갈 수 있다. 실제로 쓰는 callback 주소만 등록해 두면 code가 도착할 수 있는 곳이 그 하나로 줄어든다. 등록하지 않은 redirect_uri를 보냈을 때 거부하는지 확인하는 검사도 따로 둔다.
|
|
- listitem [ref=f28e333]:
|
|
- generic [ref=f28e334]: "07"
|
|
- generic [ref=f28e335]:
|
|
- heading "로그인 구간과 API 호출 구간을 한 줄로 그리지 않는다" [level=3] [ref=f28e336]
|
|
- paragraph [ref=f28e337]: 로그인 구간은 authorization request에서 시작해 callback과 code 교환을 지나 로그인 상태를 만드는 데까지다. API 호출 구간은 브라우저 입력, 중간 계층의 credential 변환, 보호 자원의 검증, 최종 응답이다. 두 구간을 한 줄로 이어 그리면 누가 code를 바꾸고 누가 API를 부르는지가 겹쳐 보인다. 구조가 갈리는 자리가 바로 여기라서 나눠서 그려야 한다.
|
|
- region [ref=f28e338]:
|
|
- heading "적용할 때" [level=2] [ref=f28e339]
|
|
- list [ref=f28e340]:
|
|
- listitem [ref=f28e341]: Authorization Code Flow를 쓰는 client를 설정하거나 문서로 설명할 때
|
|
- listitem [ref=f28e342]: 브라우저 요청과 server-to-server 요청이 한 흐름에 섞여 있을 때
|
|
- listitem [ref=f28e343]: endpoint별로 무엇이 노출되는지 나눠야 할 때
|
|
- listitem [ref=f28e344]: PKCE와 client 인증의 자리를 정할 때
|
|
- region [ref=f28e345]:
|
|
- heading "예외와 주의" [level=2] [ref=f28e346]
|
|
- list [ref=f28e347]:
|
|
- listitem [ref=f28e348]: Client Credentials처럼 사용자 없이 token을 받는 흐름은 Authorization Endpoint를 지나지 않는다.
|
|
- listitem [ref=f28e349]: Device Authorization Grant는 브라우저 redirect 대신 별도의 사용자 code 단계를 쓴다. redirect_uri 항목이 그대로 적용되지 않는다.
|
|
- region [ref=f28e350]:
|
|
- heading "예시" [level=2] [ref=f28e351]
|
|
- list [ref=f28e352]:
|
|
- listitem [ref=f28e353]: authorization request에는 code_challenge_method=S256이 있고 client secret은 없다
|
|
- listitem [ref=f28e354]: token request에는 code_verifier가 있다. secret을 가진 client는 이 요청에서 자기를 인증한다
|
|
- listitem [ref=f28e355]: expected issuer는 http://localhost:8080/realms/keycloak-patterns 이고 JWK 조회는 컨테이너 network 주소를 쓴다
|
|
- listitem [ref=f28e356]: audience에 keycloak-pattern-api 가 없으면 invalid_token 결과가 되어 401이 된다
|
|
- listitem [ref=f28e357]: redirect allowlist에 wildcard가 있으면 등록한 host의 다른 경로로도 code가 갈 수 있다
|
|
- paragraph [ref=f28e358]: 마지막 검증 2026.08.25
|
|
- region [ref=f28e359]:
|
|
- paragraph [ref=f28e360]: Relations
|
|
- heading "이 기록과 연결된 맥락" [level=2] [ref=f28e361]
|
|
- list [ref=f28e362]:
|
|
- listitem [ref=f28e363]:
|
|
- link "브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다. SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계" [ref=f28e364] [cursor=pointer]:
|
|
- /url: /cases/spa-browser-credential-boundary
|
|
- generic [ref=f28e365]: 브라우저가 code를 직접 교환하는 흐름에서 endpoint별 이동을 관측했다.
|
|
- strong [ref=f28e366]: SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계
|
|
- generic [ref=f28e367]: ↗
|
|
- listitem [ref=f28e368]:
|
|
- link "confidential client가 token endpoint에서 자기 client를 인증하는 실례다. Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f28e369] [cursor=pointer]:
|
|
- /url: /cases/split-custody-access-token
|
|
- generic [ref=f28e370]: confidential client가 token endpoint에서 자기 client를 인증하는 실례다.
|
|
- strong [ref=f28e371]: Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출
|
|
- generic [ref=f28e372]: ↗
|
|
- complementary [ref=f28e373]:
|
|
- heading "작업 상태" [level=2] [ref=f28e374]
|
|
- status "편집 상태" [ref=f28e375]: 저장됨
|
|
- generic [ref=f28e376]:
|
|
- generic [ref=f28e377]:
|
|
- term [ref=f28e378]: 저장 버전
|
|
- definition [ref=f28e379]: "34"
|
|
- generic [ref=f28e380]:
|
|
- term [ref=f28e381]: 종류
|
|
- definition [ref=f28e382]: REFERENCE
|
|
- paragraph [ref=f28e383]: 불완전한 초안도 저장할 수 있습니다. Ctrl+S 로도 저장합니다. 게시를 누르면 채워야 할 칸을 그 자리에 표시합니다.
|
|
- generic [ref=f28e384]:
|
|
- button "저장" [disabled] [ref=f28e385]
|
|
- button "게시" [ref=f28e386]
|
|
- paragraph [ref=f28e387]: 버전 34으로 저장했습니다. |