- 계약 채택 — 독자 질문, 후보 29건(PROMOTE 24 · MERGE_INTO 4 · KEEP_IN_SSOT 1). 게시 중 17건은 전부 유지. 저장소 keycloak-pattern 은 패턴 넷이 브랜치로 갈라져 있어 revisions 로 tip 넷을 적었다. keycloak-session-store 는 같은 저장소 @ cdac9b8 - 게시된 기록의 redirect_uri 가 SSOT·코드와 달랐다 — OAuth2callback.html → callback.html (frontend/src/app.js 에서 확인). 계약 title 이 기록과 다른 7건도 기록 쪽으로 맞췄다 - 미작성 1건 작성 — 패턴 검증을 실제로 돌릴 때의 안전한 순서(Reference) - 리뷰 100건 반영 — 설명 뒤에 붙은 평가·차례 예고·독자 오해 가정·작성 지시를 지웠다. 삭제가 남긴 조각 4건을 고치고, 원래부터 잘려 있던 로컬 미리보기 라벨 1건도 닫았다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
111 lines
7.6 KiB
Markdown
111 lines
7.6 KiB
Markdown
---
|
|
id: ede6b9ce-eeed-40c8-9175-9e8116029395
|
|
kind: REFERENCE
|
|
slug: public-confidential-client-boundary
|
|
title: Public Client와 Confidential Client 구분 기준
|
|
topic: oauth-oidc-auth-boundary
|
|
topicName: OAuth/OIDC 인증 경계
|
|
project: KeyCloak Patterns
|
|
status: 게시 중
|
|
version: 27
|
|
verifiedOn: 2026-08-30
|
|
studio: "https://hyeonworks.com/studio/documents/ede6b9ce-eeed-40c8-9175-9e8116029395/edit"
|
|
public: "https://hyeonworks.com/references/public-confidential-client-boundary"
|
|
sourceRevision: keycloak-patterns-lab@2026-08
|
|
source:
|
|
- final/document.md#검토한-선택지와-막힌-지점-책임과-데이터
|
|
- final/document.md#선택의-이유와-지킨-경계-ap1
|
|
- final/document.md#선택의-이유와-지킨-경계-ap2
|
|
---
|
|
|
|
# Public Client와 Confidential Client 구분 기준
|
|
|
|
OAuth 클라이언트의 종류는 클라이언트 시크릿을 안전하게 보관할 수 있는지로 정한다.
|
|
SPA(Single Page Application)는 브라우저에서 실행되기 때문에 시크릿을 사용자에게 노출하지 않고 보관할 방법이 없다.
|
|
그래서 SPA는 대개 public client로 등록한다.
|
|
|
|
## 관계
|
|
|
|
- **SPA에서 토큰을 직접 관리하면서 드러난 Browser Credential 경계**
|
|
SPA를 public client로 등록한 이유를 실제 구성에서 확인할 수 있다.
|
|
- **Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출**
|
|
confidential client로 등록해도 액세스 토큰을 브라우저까지 보낼지는 따로 정한다.
|
|
- **Authorization Code Flow의 Endpoint와 Credential 이동 기준**
|
|
클라이언트 종류에 따라 token endpoint에서 하는 클라이언트 인증이 달라진다.
|
|
|
|
## 목적
|
|
|
|
OAuth 클라이언트를 등록하려면 이 클라이언트를 public client로 볼지 confidential client로 볼지부터 정해야 한다.
|
|
그래야 authorization code를 토큰으로 교환할 때 PKCE(Proof Key for Code Exchange)를 쓸지, 클라이언트 시크릿으로 클라이언트를 인증할지 정할 수 있다.
|
|
|
|
클라이언트 종류를 나누는 기준은 클라이언트 시크릿을 사용자에게 노출하지 않고 안전하게 보관할 수 있는지다.
|
|
SPA는 브라우저에서 실행되기 때문에 코드에 시크릿을 넣어도 개발자 도구 같은 것으로 사용자가 확인할 수 있고, 그래서 시크릿을 안전하게 보관할 수 없는 public client로 구성한다.
|
|
|
|
서버나 BFF(Backend for Frontend)는 시크릿을 서버 안에만 두고 브라우저에 전달하지 않을 수 있으므로 confidential client로 구성할 수 있다.
|
|
|
|
클라이언트 종류가 토큰을 누가 보관하는지까지 정해 주지는 않는다.
|
|
클라이언트 종류는 시크릿을 안전하게 보관할 수 있는지로 정하고, 토큰을 브라우저와 서버 중 어디에서 관리할지는 애플리케이션의 인증 구조에 따라 따로 정한다.
|
|
|
|
## 규칙
|
|
|
|
### 1. 클라이언트 시크릿을 숨길 수 있는지로 종류를 정한다
|
|
|
|
애플리케이션의 배포 파일이나 실행 중인 메모리에서 사용자가 클라이언트 시크릿을 확인할 수 있다면 그 시크릿은 안전하게 보관할 수 없으므로 public client로 본다.
|
|
시크릿을 서버 안에만 보관하고 사용자에게 전달되지 않도록 통제할 수 있다면 confidential client로 구성할 수 있다.
|
|
|
|
네이티브 앱은 브라우저에서 실행되지는 않지만 애플리케이션이 사용자 기기에 설치되기 때문에, 배포 파일을 분석하면 안에 들어 있는 클라이언트 시크릿을 확인할 수 있다. 그래서 네이티브 앱도 대개 public client로 다룬다.
|
|
|
|
### 2. public client에서 Authorization Code Flow에 PKCE를 함께 쓴다
|
|
|
|
PKCE는 authorization code가 중간에 탈취되더라도 다른 사람이 그 code를 토큰으로 바꾸기 어렵게 만드는 보호 장치다.
|
|
클라이언트 시크릿을 대신해 클라이언트를 인증하는 방식은 아니다.
|
|
|
|
로그인을 시작할 때 클라이언트는 임의의 code_verifier를 만들고, 이 값을 변환한 code_challenge를 authorization request에 함께 보낸다. 이후 authorization code를 토큰으로 교환할 때 원래의 code_verifier를 제출한다.
|
|
Authorization Server는 처음 받은 code_challenge와 맞춰 보고 같은 요청에서 시작된 교환인지 확인한다.
|
|
|
|
변환 방식은 S256을 쓴다. plain은 code_verifier 자체가 code_challenge로 전달되기 때문에 authorization request를 관찰한 사람이 그 값을 그대로 알 수 있다. S256은 code_verifier를 SHA-256으로 변환한 값을 보내므로 authorization request에 원래의 code_verifier가 실리지 않는다.
|
|
|
|
### 3. confidential client에도 PKCE를 함께 쓸 수 있다
|
|
|
|
클라이언트 인증과 PKCE는 보호하는 대상이 다르기 때문에 confidential client에서 둘을 같이 쓸 수 있다.
|
|
클라이언트 인증은 token endpoint에 요청을 보낸 쪽이 그 클라이언트가 맞는지 확인하고, PKCE는 authorization code를 받은 쪽이 로그인을 시작할 때 만든 code_verifier를 가지고 있는지 확인한다.
|
|
|
|
### 4. public client에서는 implicit flow와 direct access grant를 끈다
|
|
|
|
implicit flow는 authorization code를 거치지 않고 액세스 토큰을 브라우저의 redirect URI로 바로 전달하기 때문에, 토큰이 지나가는 구간과 노출될 수 있는 범위가 넓어진다.
|
|
|
|
direct access grant는 애플리케이션이 사용자의 아이디와 비밀번호를 직접 받아 Authorization Server에 전달하는 방식이다.
|
|
사용자가 IdP에만 주면 되는 비밀번호를 애플리케이션도 함께 다룬다.
|
|
|
|
현재 구조는 Authorization Code Flow를 쓰므로 implicit flow와 direct access grant는 비활성화했다.
|
|
|
|
### 5. 클라이언트 종류만으로 브라우저가 토큰을 받는지가 정해지지는 않는다
|
|
|
|
confidential client가 authorization code를 토큰으로 교환하더라도, 그렇게 받은 액세스 토큰을 다시 브라우저에 전달하는 구조를 만들 수 있다.
|
|
|
|
클라이언트 종류는 클라이언트 시크릿을 어디에 안전하게 보관할 수 있는지를 말한다.
|
|
액세스 토큰이 브라우저까지 가는지는 어느 계층이 실제 API 호출을 맡도록 설계했는지에 따라 따로 정해진다.
|
|
|
|
이 기준으로 등록한 클라이언트 넷 중 셋이 confidential인데, 그 셋에서 액세스 토큰이 브라우저까지 가는지는 갈렸다.
|
|
mediator 구성에서는 브라우저가 API를 직접 불러서 액세스 토큰이 응답으로 내려갔고, BFF와 프록시 구성에서는 서버 쪽이 API를 불러서 브라우저에 줄 것이 없었다.
|
|
|
|
## 적용 조건
|
|
|
|
- 새 OAuth 클라이언트를 등록할 때
|
|
- SPA와 서버 중 어디가 code를 교환할지 정할 때
|
|
- PKCE와 클라이언트 인증을 어디에 걸지 정할 때
|
|
- 기존 클라이언트의 종류가 맞는지 다시 볼 때
|
|
|
|
## 예외
|
|
|
|
- 같은 서비스가 브라우저용 public client와 서버용 confidential client를 따로 등록할 수 있다.
|
|
- 백엔드가 사용자 없이 자기 자격으로 부르는 흐름은 Client Credentials를 쓰는 별도 클라이언트다.
|
|
|
|
## 예시
|
|
|
|
- SPA용 클라이언트 : public, standard flow만 켜고 implicit flow와 direct grant는 끈다
|
|
- mediator용 클라이언트 : confidential, client_secret_basic으로 token endpoint에서 인증한다
|
|
- BFF용 클라이언트 : confidential, PKCE S256을 함께 쓴다
|
|
- 프록시용 클라이언트 : confidential, oauth2-proxy가 클라이언트 시크릿과 verifier로 code를 교환한다
|
|
- confidential client인 mediator를 써도 액세스 토큰이 브라우저 응답에 실려 나갈 수 있다
|