Files
document-haness/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-authorization-code-and-pkce.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

5.4 KiB

id, kind, slug, title, topic, topicName, project, status, version, basisVersion, studio
id kind slug title topic topicName project status version basisVersion studio
75c6c657-3e03-47a0-a9d0-5637fce9dd3f CONCEPT authorization-code-and-pkce Authorization Code와 PKCE가 보호하는 구간 oauth-oidc-auth-boundary OAuth/OIDC 인증 경계 KeyCloak Patterns 게시 전 4 Keycloak 26.7.0 · oidc-client-ts https://hyeonworks.com/studio/documents/75c6c657-3e03-47a0-a9d0-5637fce9dd3f/edit

Authorization Code와 PKCE가 보호하는 구간

authorization code는 로그인을 마친 사용자가 애플리케이션으로 돌아올 때 잠시 들고 오는 교환용 값이다. 이 code를 access token으로 바꾸는 구간을 PKCE가 보호한다. authorization request에 넣은 code_challenge와 token request에 넣은 code_verifier가 맞아야 교환이 끝난다.

관계

  • Authorization Code Flow의 Endpoint와 Credential 이동 기준 이 개념을 endpoint별 기준으로 정리한 기록이다.
  • Public Client와 Confidential Client 구분 기준 client 종류에 따라 token endpoint의 인증 방식이 달라진다.
  • SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계 브라우저가 code를 직접 교환한 구성이다.

본문

code를 한 번 더 교환하는 이유

로그인 한 번에 요청은 두 번 오간다. 브라우저가 먼저 Keycloak으로 이동하고, 로그인이 끝나면 authorization code를 들고 redirect URI로 돌아온다. 이 code로는 아직 API를 부를 수 없다. OAuth client가 code를 token endpoint에 제출해야 access token을 받는다.

교환을 나눈 덕분에 access token이 브라우저 주소창을 지나지 않는다. authorization request는 full-page navigation이라 URL이 주소창과 히스토리, Authorization Server 접근 로그에 남는다. 여기 남아도 되는 값만 code로 두고, token은 별도 요청의 body로 받는다.

authorization request에 들어가는 challenge

oidc-client-ts가 만드는 요청의 핵심 모양은 다음과 같다.

GET http://localhost:8080/realms/keycloak-patterns/protocol/openid-connect/auth
  ?client_id=spa-public
  &redirect_uri=http%3A%2F%2Flocalhost%3A8088%2Fcallback.html
  &response_type=code
  &scope=openid%20profile%20email
  &state=<opaque-state>
  &code_challenge=<opaque-challenge>
  &code_challenge_method=S256

response_type=code가 Authorization Code Flow를 쓴다는 표시이고, code_challengecode_challenge_method=S256이 PKCE 사용을 나타낸다. state와 challenge 값은 요청마다 달라진다.

state와 PKCE verifier는 redirect를 건너야 하므로 브라우저에 남는다. AP1은 이 둘을 Session Storage에 두고 Keycloak 왕복을 건넌다.

token request가 제출하는 verifier

callback으로 돌아온 code는 다음 요청으로 교환된다.

POST http://localhost:8080/realms/keycloak-patterns/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=spa-public
&code=<authorization-code>
&redirect_uri=http://localhost:8088/callback.html
&code_verifier=<original-verifier>

code_verifier는 authorization request를 시작할 때 만든 원본 값이다. Authorization Server는 challenge와 verifier가 대응하는지 확인하고 교환을 끝낸다. 이 대응이 authorization request를 시작한 client와 code를 교환하는 주체를 연결한다.

S256과 plain의 차이

verifier에서 challenge를 만드는 방법이 두 가지다.

method challenge 값 중간에서 challenge를 본 경우
plain verifier 그대로 그대로 verifier로 쓸 수 있다
S256 verifier의 SHA-256 verifier를 되돌릴 수 없다

AP1 realm은 S256을 요구한다. AP1 코드에는 createPkcePair()라는 수동 helper도 있어서 32 random bytes를 padding 없는 Base64URL verifier로 바꾸고 SHA-256 challenge와 "S256"을 반환한다. 다만 실제 signinRedirect()는 이 helper를 호출하지 않는다. helper는 UI의 PKCE demo button용이고 로그인은 pinned oidc-client-ts가 수행한다.

PKCE가 막지 않는 것

PKCE는 탈취된 authorization code의 교환을 어렵게 한다. 이미 발급된 access token을 숨기지는 않는다. 브라우저가 token을 직접 다루는 구성에서 실행 중 악성 script가 Bearer token을 보거나 사용자 권한으로 API를 부르는 문제는 PKCE 밖이다.

state도 PKCE와 다른 값이다. state는 callback이 원래 시작한 transaction의 것인지 대조하는 값이고, verifier는 code 교환 주체를 묶는 값이다.

client 종류에 따라 달라지는 부분

spa-public은 secret이 없는 public client다. token endpoint에서 client 인증을 하지 않고 PKCE만 사용한다.

confidential client는 여기에 client 인증을 더한다. AP3의 bff-confidentialclient_secret_basic으로 자기 client를 인증하면서 PKCE S256도 함께 쓴다. Spring Security에서는 OAuth2AuthorizationRequestCustomizers.withPkce()를 authorization request resolver에 장착해 framework가 state와 verifier를 만든다.

AP2 client 설정에는 S256을 강제하는 속성이 없고, AP2 테스트도 authorization request의 challenge를 검사하지 않는다. AP2에서 확인한 것은 Authorization Code Flow를 쓴다는 데까지이고, PKCE S256이 고정됐는지는 확인하지 않았다.