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>
100 lines
5.4 KiB
Markdown
100 lines
5.4 KiB
Markdown
---
|
|
id: 75c6c657-3e03-47a0-a9d0-5637fce9dd3f
|
|
kind: CONCEPT
|
|
slug: authorization-code-and-pkce
|
|
title: Authorization Code와 PKCE가 보호하는 구간
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 전
|
|
version: 4
|
|
basisVersion: Keycloak 26.7.0 · oidc-client-ts
|
|
studio: "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를 직접 교환한 구성이다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 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가 만드는 요청의 핵심 모양은 다음과 같다.
|
|
|
|
```http label="authorization request"
|
|
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_challenge`와 `code_challenge_method=S256`이 PKCE 사용을 나타낸다. `state`와 challenge 값은 요청마다 달라진다.
|
|
|
|
`state`와 PKCE verifier는 redirect를 건너야 하므로 브라우저에 남는다. AP1은 이 둘을 Session Storage에 두고 Keycloak 왕복을 건넌다.
|
|
|
|
## token request가 제출하는 verifier
|
|
|
|
callback으로 돌아온 code는 다음 요청으로 교환된다.
|
|
|
|
```http label="token request"
|
|
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-confidential`은 `client_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이 고정됐는지는 확인하지 않았다.
|
|
|
|
<!-- body:end -->
|