Files
document-haness/docs/keycloak/tech-log-studio/oauth-oidc-auth-boundary/concept/concept-authorization-code-and-pkce.md
T

99 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 인증 경계
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 -->