1. 실무에서의 Auth Server란 무엇이며 왜 Keycloak을 선택하는가
간단히 상기부터 시작하겠습니다: Auth Server (IdP)는 다음을 수행하는 서비스입니다:
- 사용자에게 로그인/등록 및 동의(consent) 화면을 제공한다;
- OAuth/OIDC 토큰(access_token, id_token, refresh_token)을 발급한다;
- 리소스 서버가 이 토큰을 검증할 수 있도록 디스커버리 문서와 JWKS 키를 게시한다.
우리 스택에서는:
- ChatGPT / MCP Jam이 OAuth 클라이언트(public client)로 동작하고;
- 여러분의 MCP 서버는 Resource Server이며;
- Keycloak은 Auth Server입니다.
Keycloak이 학습과 실무 모두에서 편리한 이유:
- 오픈 소스이며 로컬/도커로 쉽게 띄울 수 있다;
- 엔터티 모델이 비교적 명확하다: realm, clients, users, roles;
- 사실상 Keycloak에서 익힌 대부분의 설정은 Auth0/Okta/Cognito로 거의 1:1 이식된다. 핵심 아이디어가 동일하기 때문이다 — client, scopes, redirect URIs, PKCE.
중요한 아이디어: 우리는 프로젝트 전체용 “Keycloak”이 아니라 우리 ChatGPT 앱을 위한 특정 Realm을 설정합니다. 이는 MCP 클라이언트 인증을 위한 일종의 “샌드박스”입니다.
결론적으로, 강의가 끝나면 다음을 갖추게 됩니다:
- ChatGPT 애플리케이션용으로 Keycloak에 별도의 realm;
- Authorization Code + PKCE가 활성화된 public‑client;
- 최소한의 scopes와 claims;
- 해당 토큰이 여러분의 Node 기반 MCP 서버에서 어떻게 사용되는지에 대한 이해.
2. MCP 관점에서 본 Keycloak의 기본 엔터티
나중에 콘솔에서 헤매지 않도록 엔터티를 정리해 봅시다.
Realm: 설정과 사용자의 공간
Keycloak의 Realm은 자체 사용자, 클라이언트, 정책을 가진 격리된 공간입니다. 비유하자면 “비즈니스 센터의 임대 사무실”과 같습니다. 각자 사무실, 직원 목록, 출입 규칙을 갖는 셈이죠.
강의와 여러분의 첫 실제 앱에서는 별도의 realm을 만드는 것이 좋습니다. 예: giftgenius-mcp 또는 mcp-course. 이렇게 하면:
- master realm을 건드리지 않아 실수로 관리 콘솔을 망가뜨릴 위험이 줄고;
- 설정과 사용자를 서로 다른 환경(dev / staging / prod) 간에 realm의 내보내기/가져오기를 통해 재사용할 수 있습니다.
Client: 애플리케이션의 등록 정보 (ChatGPT / MCP Jam)
Keycloak의 Client는 “사용자”가 아니라 Auth Server에서 토큰을 받기 위해 접근하는 애플리케이션을 의미합니다. 우리의 경우 이는 여러분의 Next.js 백엔드가 아니라 바로 MCP 클라이언트입니다: ChatGPT, MCP Jam, 혹은 UI에서 수동으로 OAuth 플로우를 구현한다면 별도의 위젯일 수도 있습니다.
클라이언트의 핵심 필드:
- client_id — 문자열 식별자;
- 유형 (public / confidential / bearer-only);
- 활성화된 OAuth 플로우(Standard Flow, Client Credentials 등);
- 허용된 redirect URI 목록;
- scopes 목록과 protocol mappers(토큰의 claims).
ChatGPT/MCP Jam에는 public client가 필요합니다. 그 이유는:
- 클라이언트인 ChatGPT는 client_secret을 안전하게 보관할 수 없고;
- 데스크톱/브라우저 도구인 MCP Jam 역시 신뢰되지 않은 환경에서 동작하기 때문입니다.
User: 실제 사용자
User는 실제 사용자입니다. username, 비밀번호, email, 속성, 그룹, 역할을 가집니다. 누군가 Keycloak을 통해 로그인하면 해당 사용자의 sub 및 다른 정보가 토큰에 담깁니다. 이 토큰을 MCP 서버에서 검증하고, 여러분의 accountId / tenantId에 매핑합니다.
데모 수준에서는 다음이면 충분합니다:
- 한두 명의 테스트 사용자(예: alice@example.com, bob@example.com);
- 원한다면 tenant 또는 plan 같은 속성을 두어 토큰의 claims가 도구 동작에 어떻게 영향을 주는지 보여줄 수 있습니다.
3. 클라이언트 유형 선택: public, PKCE, 그리고 왜 secret이 없는가
이제 본론으로: ChatGPT/MCP에 맞춰 Keycloak에서 클라이언트를 어떻게 설정할까요.
Public vs Confidential: 왜 client_secret이 아닌가
고전적인 웹 애플리케이션에서는 백엔드를 만들고 그곳에 client_secret을 보관합니다. 그리고 서버가 IdP에 토큰을 요청합니다. 이는 confidential client로, 비밀을 보관할 수 있습니다.
ChatGPT의 세계는 반대입니다:
- OAuth 클라이언트가 바로 ChatGPT 플랫폼이거나 MCP Jam 같은 유틸이고;
- 여러분이 그 코드와 실행 환경을 통제하지 못하며;
- ChatGPT에 제공한 어떤 client_secret도 즉시 유출된 것으로 간주해야 합니다.
그래서 ChatGPT/Jam은 public clients로 동작하며 client_secret 없이, 이를 PKCE(Proof Key for Code Exchange)로 보완합니다.
PKCE를 쉬운 말로 설명하면
PKCE는 세션마다 생성되는 일회성 “비밀”입니다. 목적은 authorization code를 탈취해 다른 곳에서 토큰으로 교환하지 못하게 하는 것입니다. 흐름은 다음과 같습니다:
- 클라이언트가 임의의 문자열 code_verifier를 생성한다.
- 이를 해시(보통 SHA-256)해 code_challenge를 얻는다.
- /authorize로 리다이렉트할 때 code_challenge와 code_challenge_method=S256을 보낸다.
- 로그인 후 사용자는 redirect URI로 code를 가지고 돌아온다.
- 클라이언트는 POST /token 요청을 보내며, code와 원래의 code_verifier를 전달한다.
- Keycloak은 verifier를 해시해 challenge와 비교하고, 일치하면 토큰을 발급한다.
우리에게 중요한 점: 이 모든 것은 ChatGPT/MCP Jam이 대신 처리합니다. 우리는 Keycloak 클라이언트에서 Authorization Code + PKCE(S256)를 지원하도록 설정하고 client_secret을 요구하지 않도록 하기만 하면 됩니다.
4. MCP 시나리오용 Keycloak 단계별 설정
정리하면, ChatGPT/MCP Jam에는 public client가 필요하고, Authorization Code Flow와 PKCE(S256)를 사용하며 client_secret은 사용하지 않습니다. 이 설정이 Keycloak에서 어떻게 보이는지 살펴봅시다.
이미 Keycloak이 동작 중이라고 가정합니다(Docker 컨테이너든 로컬 설치든 상관없습니다). 지금은 어디를 클릭하는지가 아니라 설정의 개념을 이해하는 것이 중요합니다.
앱을 위한 새 realm 만들기
giftgenius-mcp라는 realm을 만듭니다. 이곳에는 다음이 포함됩니다:
- ChatGPT 애플리케이션 전용 사용자들;
- ChatGPT/MCP Jam이 OAuth를 진행할 클라이언트들;
- 고유의 비밀번호 및 토큰 정책.
실용 팁: 관리 콘솔(관리자) 인증에 사용하는 realm과 ChatGPT 클라이언트용 realm을 섞지 마세요. 그게 더 안전하고 논리적으로도 단순합니다.
테스트 사용자 추가
예를 들어 alice 사용자를 만듭니다:
- username: alice;
- email: alice@example.com;
- 비밀번호 설정(단순화를 위해 복잡한 정책은 생략);
- 원하면 tenantId=demo-tenant 속성이나 ROLE_PREMIUM 역할을 추가.
이후 MCP 서버에서 토큰을 디코드해 sub, email, tenantId를 추출하고 여러분의 사용자 모델과 연결할 수 있습니다.
MCP Jam / ChatGPT용 public client 생성
이제 핵심인 Client 설정입니다.
개념적으로는 다음과 같이 설정합니다:
- Client ID: giftgenius-mcp-client (원하는 이름);
- 유형: public / Client Authentication off;
- Standard Flow(Authorization Code) 활성화;
- S256 방법의 PKCE 활성화;
- redirect URI 구성;
- 필요한 scopes 설정(openid + 커스텀 예: mcp:tools).
Standard Flow와 PKCE 활성화
개념적으로:
- Authorization Code Flow 활성화(보통 “Standard Flow Enabled”라고 표시됨);
- PKCE 섹션에서 pkceRequired=true를 설정하고, 보통 code_challenge_method=S256을 명시합니다.
왜 S256인가: 최신 OAuth 2.1 문서와 OpenAI/Model Context Protocol 권장사항에서는 S256을 안전한 방법으로 지원합니다. plain‑PKCE는 안전하지 않은 것으로 간주됩니다.
Redirect URI — 가장 깨지기 쉬운 부분
Redirect URI는 클라이언트가 사용할 값과 글자 그대로 일치해야 합니다. 그렇지 않으면 인증 단계에서 invalid_redirect_uri 오류를 보게 됩니다.
이 강의에는 두 가지 전형적인 클라이언트가 있습니다:
- MCP Jam/Inspector (디버깅용). 보통 http://localhost:PORT/...에서 동작합니다. 로컬 시나리오에서는 다음과 같은 redirect를 허용하는 것이 합리적입니다:
- http://localhost:5173/* 또는 Jam이 사용하는 구체적인 경로.
- ChatGPT / Apps SDK (프로덕션). 여기서는 redirect URI를 플랫폼이 결정합니다. 실제 통합에서는 OpenAI의 최신 문서를 참고해 ChatGPT가 콜백으로 사용할 URL을 등록합니다.
이 강의의 핵심은: ChatGPT는 임의의 redirect를 사용할 수 없고, 반드시 Auth Server에 등록된 값과 일치해야 한다는 것입니다. 따라서:
- * 등으로 “모든 URL 허용”을 절대 사용하지 말 것;
- 로컬 개발에서는 localhost 범위에서의 와일드카드는 허용될 수 있지만, 프로덕션에서는 사용하지 말 것.
Scopes: 최소하지만 충분하게
Scopes는 클라이언트가 요청하는 권한의 미니 목록입니다.
MCP 시나리오에서 보통 필요한 것은 다음과 같습니다:
- openid — OpenID Connect를 활성화해 id_token과 sub(때로는 email)을 받기 위함;
- 커스텀 scope(예: mcp:tools) — “MCP 도구 접근 허용”을 의미합니다.
Keycloak에서는 Client Scopes를 통해 다음과 같이 구성할 수 있습니다:
- openid 유지;
- 필요 없다면 기본 profile, email 같은 불필요한 scope 비활성화;
- 새 scope mcp:tools 추가 — 이후 Resource Server에서 도구 접근을 이 scope로 제한합니다.
이것이 중요한 이유 두 가지:
- openid가 없으면 id_token과 일부 표준 OIDC 필드를 받지 못합니다.
- 별도의 커스텀 scope가 없으면 MCP 서버에서 “이 토큰으로 내 도구를 호출할 수 있다”를 명확히 판단하기 어렵습니다.
5. 토큰 설정: 수명, 서명, 그리고 claims
이제 Keycloak이 발급하는 토큰과 MCP 시나리오에 맞춘 설정을 살펴봅니다.
Access token 수명
Keycloak의 realm 설정에는 Tokens 섹션이 있으며, 여기서 다음을 지정할 수 있습니다:
- Access Token Lifespan;
- Refresh Token Lifespan 및 기타 타임아웃.
ChatGPT 앱에는 단명 access token이 중요합니다:
- 몇 분 또는 몇 시간 정도가 일반적;
- 토큰이 만료되면 MCP 서버가 401을 반환하고, ChatGPT가 OAuth 플로우를 다시 시작하며 필요한 경우 사용자가 다시 로그인합니다.
아이디어는 OpenAI Apps SDK 문서와 동일합니다: 짧은 TTL + 토큰 갱신, 그리고 IdP 측에서 토큰을 철회해 비교적 빠르게 사용자를 “로그아웃”시킬 수 있어야 합니다.
ChatGPT 클라이언트에서 refresh token은 대체로 중요도가 낮거나, 영구 세션을 피하기 위해 짧게 발급됩니다.
토큰에 포함되길 원하는 claims
최소한 다음이 필요합니다:
- sub — Keycloak에서의 사용자 고유 식별자;
- iss — 토큰 발급자(issuer);
- aud — 토큰의 대상 리소스(이후 MCP 서버에서 사용);
- exp — 만료 시간;
- scope — scopes 목록.
추가로 자주 유용한 것:
- email — 사용자의 이메일을 보고 싶다면;
- tenantId 또는 유사 claim — 멀티 테넌시 시나리오;
- roles — 추가 인가를 위해.
Keycloak에서는 Protocol Mappers로 설정합니다:
- email, preferred_username 등 표준 매퍼;
- 사용자 속성용 커스텀 매퍼(user.attribute → claim.name).
예: email을 claim으로 토큰에 추가하는 매퍼에서 user.attribute=email, claim.name=email로 지정합니다.
MCP 서버 측에서는 파싱된 JWT의 claims를 사용해 다음을 수행할 수 있습니다:
- sub를 여러분의 accountId에 연결;
- tenantId를 사용해 해당 테넌트 소유 데이터만 조회;
- roles로 보다 “세밀한” 권한을 구분.
토큰 서명과 JWKS
Keycloak은 기본적으로 비대칭 알고리즘(보통 RS256)으로 access/id 토큰에 서명하고, OpenID Discovery 문서의 JWKS endpoint를 통해 공개 키를 게시합니다.
이는 MCP 서버가 다음을 수행할 수 있게 해줍니다:
- 토큰에서 issuer를 읽고;
- /.well-known/openid-configuration에서 JWKS endpoint를 찾고;
- 공개 키를 받아 로컬에서 토큰 서명을 검증합니다.
이 부분은 MCP 서버를 보호된 리소스로 만드는 다음 강의에서 더 자세히 다루지만, Keycloak이 왜 이러한 메타데이터를 제공하는지 지금 이해해두면 좋습니다.
6. Dynamic Client Registration (DCR): 언제 필요할까
이 섹션은 다소 고급 주제입니다. 지금까지는 클라이언트를 관리 콘솔에서 “수동으로” 설정했으며, 앱을 실행하기에는 충분합니다. 하지만 OAuth 프로토콜은 별도의 endpoint를 통해 클라이언트가 동적으로 등록되도록 허용합니다.
ChatGPT와 MCP 맥락에서 OpenAI는 플랫폼이 Dynamic Client Registration을 사용할 수 있다고 명시합니다. 즉, ChatGPT가 디스커버리 문서의 registration_endpoint를 통해 Auth Server에 “온더플라이”로 자신을 등록할 수 있습니다.
Keycloak에서는 다음과 같은 방식입니다:
- realm 수준에서 DCR을 활성화하고;
- 누가 어떤 grant types/scopes로 새 클라이언트를 등록할 수 있는지에 대한 정책을 구성합니다.
Authorization Code + PKCE와 scope openid mcp:tools를 갖는 public client 등록 예제 JSON은 다음과 같을 수 있습니다:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
여기서 tokenEndpointAuthMethod: "none" 은 client_secret이 없는 public client를 의미합니다.
이 강의에서는 다음 정도만 알면 충분합니다:
- DCR은 클라이언트가 많거나 수명이 짧을 때 유용하다;
- ChatGPT가 여러분의 IdP에 스스로 등록할 수도 있다;
- 초기에는 UI에서 생성한 정적 클라이언트만으로도 충분하다.
7. 우리 학습용 애플리케이션과의 연결
우리는 학습용 MCP 서버(예: GiftGenius)가 있으며, 이 서버는 다음을 수행합니다:
- 가능한 선물 목록을 제공;
- 사용자의 위시리스트를 저장;
- 나중에는 커머스 기능과 연동해 주문 처리 등을 수행.
보호되지 않은 MCP 서버는 누가 요청하는지 알지 못합니다:
- ChatGPT의 요청이 논리적으로 “Alice” 혹은 “Bob”으로부터일 수 있지만 MCP 서버는 이를 구분하지 못하고;
- 개인화된 선물 기록을 보여줄 수 없으며;
- 올바른 계정에서 안전하게 결제할 수도 없습니다.
Keycloak을 Auth Server로 설정하면 상황이 달라집니다:
- ChatGPT는 여러분의 MCP 리소스의 .well-known을 보고 보호되어 있으며 토큰이 필요함을 이해합니다.
- ChatGPT는 Authorization Code + PKCE 플로우로 사용자를 Keycloak으로 보냅니다.
- 사용자가 로그인합니다(우리의 alice).
- ChatGPT는 sub, email, mcp:tools 등 claims가 담긴 access token을 받습니다.
- ChatGPT는 이제 Authorization: Bearer <token> 헤더로 GiftGenius 도구를 호출합니다.
- MCP 서버는 토큰을 검증해 “좋아, 이건 sub=... 및 tenantId=demo-tenant를 가진 Alice”임을 파악하고 그에 맞게 응답합니다.
이 연결 고리는 다음 강의에서 마무리됩니다. MCP 서버를 “진짜” 리소스 서버로 만들고, 메타데이터 엔드포인트, 토큰 검증, 사용자 매핑을 구현합니다.
8. 작은 실전 예시(우리 스택: TypeScript + Node)
아래 내용은 “유일하게 정답인” 방식이 아니라, 일반적인 Node/TypeScript 스택에서 어떻게 보일 수 있는지에 대한 레퍼런스입니다. 지금은 Keycloak 클릭 설정에 집중하고 있다면, 이 섹션은 가볍게 읽고 MCP 서버 연결 단계에서 다시 돌아와도 됩니다.
Keycloak 설정은 주로 UI에서 클릭으로 하거나 Admin REST API로 처리하지만, MCP 서버 측에서 이를 어떻게 사용할지 감을 잡을 수 있도록 몇 가지 코드 조각을 보여드리겠습니다.
이미 공식 SDK 기반의 Node.js MCP 서버(TypeScript)가 있다고 가정합니다.
인증 설정(issuer와 audience)
authConfig.ts라는 작은 모듈을 만듭니다:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // MCP 서버의 URL
requiredScopes: ['mcp:tools'], // 토큰에서 기대하는 최소 범위
};
여기서 issuer는 Keycloak realm의 URL이고, audience는 리소스 식별자입니다(토큰과 MCP 설정에서 사용).
JWKS를 이용한 기본 JWT 검증
실무에서는 보통 jsonwebtoken + jwks-rsa 같은 라이브러리나 MCP SDK의 유틸을 사용합니다. 가장 단순한 스켈레톤은 다음과 같습니다:
// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';
const client = jwksClient({
jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export function verifyAccessToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: authConfig.audience,
issuer: authConfig.issuer,
},
(err, decoded) => (err ? reject(err) : resolve(decoded)),
);
});
}
물론 에러 처리와 키 캐싱은 더 정교해야 하지만, 핵심은 분명합니다: Keycloak이 JWKS 키를 게시하고, 우리는 그 키로 서명을 검증합니다.
scope 확인과 identity 추출
MCP 도구용 미들웨어에서는 다음과 같이 구현할 수 있습니다:
// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';
export async function requireAuth(bearerToken: string) {
const token = bearerToken.replace(/^Bearer\s+/i, '');
const decoded: any = await verifyAccessToken(token);
const scopes = (decoded.scope as string).split(' ');
const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return {
userId: decoded.sub,
email: decoded.email,
tenantId: decoded.tenantId,
};
}
그 다음 MCP 도구 핸들러에서 userId와 tenantId를 사용해 해당 사용자의 선물 목록 등을 로드하면 됩니다. 도구 자체는 이전 모듈에서 이미 구현했으며, 지금은 Keycloak의 토큰이 백엔드에서 이해 가능한 identity로 바뀌는 흐름만 보면 됩니다.
9. MCP Auth Server로 Keycloak을 설정할 때 흔한 실수
오류 1: client_secret이 있는 confidential client를 사용함.
관성대로 confidential 유형의 클라이언트를 만들고 client_secret을 MCP/ChatGPT 구성에 넣으려는 경우가 있습니다. ChatGPT App 생태계에서는 이것이 작동하지 않으며 안전하지도 않습니다. ChatGPT는 public client이므로 비밀을 보관할 수 없습니다. 올바른 방식은 public client + PKCE입니다.
오류 2: 기본 scopes를 너무 넓게 설정함.
기본으로 켜진 profile, email 등 표준 scopes를 그대로 두고 모든 채팅에 그런 토큰을 발급하는 것은 좋지 않습니다. 최소화하세요: openid와 구체적 mcp:tools(또는 소수의 업무 관련 scopes)면 초기 버전에는 충분합니다. 이렇게 하면 과도한 데이터 유출 위험이 줄고, 동작이 더 예측 가능해집니다.
오류 3: 잘못된 redirect URI.
클래식 사례: Keycloak에는 http://localhost:5173/callback을 등록했는데 MCP Jam은 http://localhost:5173/로 요청합니다(또는 반대). 결과는 invalid_redirect_uri와 매우 난감한 디버깅입니다. Jam/ChatGPT 문서의 redirect URI를 항상 정확히 확인하고, 철자 하나까지 동일하게 등록하세요.
오류 4: PKCE를 끄거나 잘못된 방법으로 켬.
일부 Keycloak 버전에서는 “PKCE required”를 별도로 켜고 방법을 S256으로 지정해야 합니다. 그렇지 않으면 PKCE를 기대하는 ChatGPT/Jam이 invalid_request 오류(code_challenge 문제)를 받을 수 있습니다. public 클라이언트의 PKCE 설정을 반드시 확인하세요.
오류 5: 토큰에 필요한 claims가 없거나 잘못됨.
해당 scope나 protocol mapper가 설정되지 않아 토큰에 sub 또는 email이 없는 경우가 있습니다. 그 결과 MCP 서버에서 토큰을 보더라도 실제 사용자에 매핑하지 못합니다. 해결: 필요한 필드(최소 sub, 가능하면 email/tenantId)가 access/id 토큰에 매핑되었는지 확인하세요.
오류 6: access token의 TTL이 지나치게 김.
보안 관점에서 access token을 하루/일주일 단위로 발급하는 것은 좋지 않습니다. 토큰이 유출되면 공격자가 MCP 리소스에 장기간 접근할 수 있습니다. access token은 단명(분 또는 시간 단위)으로 하고, 필요 시 재인증을 사용하세요.
오류 7: realm 혼동 및 master 사용.
가장 먼저 master realm에 바로 클라이언트와 사용자를 만드는 경우가 종종 있습니다. 그러다 프로젝트를 몇 개 더 붙이면 무엇이 어디에 속하는지 모호해집니다. 처음부터 앱/강의별로 별도 realm을 만드는 것이 좋습니다. 여러분과 DevOps 모두에게 삶이 편해집니다.
GO TO FULL VERSION