Table of contents
- 세 역할 — Resource Server, Authorization Server, Client
- Authorization Server를 찾는 흐름
- 클라이언트 등록의 세 가지 길
- 인증 흐름 — 한 번 끝까지
- 토큰을 누구에게 — Resource Indicator (RFC 8707)
- Refresh, scope, step-up
- 흔한 함정 둘
세 역할 — Resource Server, Authorization Server, Client
OAuth 2.1의 표준 분업이 그대로 매핑된다.
- MCP Server (Resource Server) — 보호받는 자원(=tools, resources, prompts)을 가진 쪽. 들어온 토큰을 검증하고, 자기에게 발급된 토큰만 받아들인다.
- MCP Client (OAuth Client) — 토큰을 받아 MCP 서버에 요청을 보내는 쪽. 사용자(resource owner)를 대신해 자원에 접근한다.
- Authorization Server — 토큰을 발급하는 쪽. 사용자 인증과 동의 화면을 책임진다. MCP 서버와 같은 곳에 호스트되어도 되고, 별도 서비스여도 된다.
세 역할을 분리해놓으면 MCP 서버 자체가 사용자 비밀번호를 알 필요가 없다. 인증은 Authorization Server가 처리하고, MCP 서버는 유효한 토큰이 자기 앞으로 발급된 것인지만 본다.
Authorization Server를 찾는 흐름
토큰을 받으려면 클라이언트가 먼저 어디서 토큰을 받아야 하는지를 알아야 한다. spec은 두 가지 발견 메커니즘을 정의한다.
1. WWW-Authenticate 헤더 (가장 자연스러운 길)
토큰 없이 MCP 서버에 요청하면 401이 돌아온다. 그 응답에 자원 메타데이터 URL이 들어 있다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
2. Well-Known URI (폴백)
401 응답이 없거나 헤더가 없으면 클라이언트가 직접 well-known 경로로 메타데이터를 시도한다.
https://example.com/.well-known/oauth-protected-resource/public/mcp(서브패스)https://example.com/.well-known/oauth-protected-resource(루트)
어느 쪽이든 자원 메타데이터가 돌아오고, 그 안에 authorization_servers 필드로 어떤 Authorization Server를 쓸지 알려준다.
sequenceDiagram
participant C as Client
participant M as MCP Server (RS)
participant A as Authorization Server
C->>M: MCP 요청 (토큰 없음)
M-->>C: 401 + WWW-Authenticate (resource_metadata URL)
C->>M: GET .well-known/oauth-protected-resource
M-->>C: { "authorization_servers": ["https://auth.example.com"] }
C->>A: GET .well-known/oauth-authorization-server
A-->>C: AS 메타데이터 (token endpoint, scopes 등)
Note over C,A: OAuth 2.1 인가 흐름
C->>M: MCP 요청 + Bearer 토큰
M-->>C: MCP 응답
Authorization Server 메타데이터는 RFC 8414 또는 OpenID Connect Discovery 1.0을 따른다. 클라이언트는 둘 다 시도해야 한다.
클라이언트 등록의 세 가지 길
Authorization Server에 이 클라이언트가 누구인지를 알리는 방법이 셋이다.
| 방식 | 언제 쓰나 |
|---|---|
| Pre-registration | 클라이언트와 서버가 사전 관계가 있을 때 |
| Client ID Metadata Documents | 사전 관계 없는 일반 케이스 (가장 흔함) |
| Dynamic Client Registration (RFC 7591) | 폴백·구버전 호환 |
가장 흥미로운 게 Client ID Metadata Documents다. 클라이언트가 자기 메타데이터를 HTTPS URL에 호스팅해두고, 그 URL 자체를 client_id로 쓴다.
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"redirect_uris": ["http://127.0.0.1:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"]
}
Authorization Server는 URL 형식의 client_id를 보면 그 URL을 fetch해서 메타데이터를 받아온다. 사전 등록이 필요 없으니, MCP 같은 클라이언트와 서버가 무수히 많은 조합으로 만나는 환경에서 자연스러운 길이 된다.
인증 흐름 — 한 번 끝까지
위의 발견을 마쳤으면 표준 OAuth 2.1 Authorization Code + PKCE 흐름이 그대로 돈다.
sequenceDiagram
participant Browser
participant Client
participant AS as Authorization Server
participant RS as MCP Server (RS)
Note over Client: PKCE code_verifier·code_challenge 생성<br/>resource 파라미터 준비
Client->>Browser: 인가 URL 열기 (code_challenge + resource)
Browser->>AS: 인가 요청
Note over AS: 사용자 로그인 + 동의 화면
AS->>Browser: redirect to callback (authorization code)
Browser->>Client: code 전달
Client->>AS: token request (code + code_verifier + resource)
AS-->>Client: access token (+ refresh token)
Client->>RS: MCP 요청 + Authorization: Bearer ...
RS-->>Client: MCP 응답
핵심 디테일 셋.
- PKCE는 필수. 옛 OAuth의 client secret 대신 매 요청마다 새 verifier·challenge 쌍을 만든다.
S256챌린지 메서드만 허용. PKCE를 지원하지 않는 Authorization Server는 MCP 클라이언트가 거부해야 한다. - Authorization 헤더로만 토큰을 보낸다. URL 쿼리 스트링에 토큰을 박으면 안 된다(서버 로그·Referer 헤더로 새기 쉬워서).
- redirect URI는 사전 등록. Authorization Server가 정확히 일치하는 값만 받아들여 phishing redirect를 막는다.
토큰을 누구에게 — Resource Indicator (RFC 8707)
OAuth의 고전적 함정이 있다. 토큰이 누구를 위한 것인지가 모호하면, A 서비스용 토큰이 B 서비스에서 받아들여져 권한이 새는 일이 생긴다(confused deputy). MCP는 RFC 8707 Resource Indicators를 필수로 박아 이 구멍을 막는다.
클라이언트가 인가·토큰 요청 둘 다에 resource 파라미터를 같이 보낸다.
&resource=https%3A%2F%2Fmcp.example.com
Authorization Server는 이 값을 토큰의 audience(aud claim)로 박아 발급한다. MCP 서버는 받은 토큰의 audience가 자기인지를 검증하고, 다르면 거부한다.
이 검증을 빼먹으면 어떤 일이 일어나는가. 같은 Authorization Server가 발급한 다른 서비스용 토큰을 MCP 서버가 받아들이게 된다. 사용자가 service-A에 동의해 발급된 토큰이 service-B(=MCP 서버)에 통과되면, 사용자가 동의한 적 없는 자원에 접근이 일어난다. spec은 이걸 MCP 서버는 자기 앞으로 발급된 토큰만 받아들여야 한다고 강하게 명문화한다.
같은 맥락에서 token passthrough도 금지다. MCP 서버가 받은 토큰을 그대로 들고 외부 API를 호출하면 안 된다 — 외부 API 입장에선 어떻게 검증된 토큰인지 알 수 없어서 같은 confused deputy 패턴이 한 단계 뒤에서 다시 일어난다. MCP 서버가 외부 API를 호출하려면 별도의 토큰을 자기 책임으로 받아야 한다.
Refresh, scope, step-up
긴 세션을 위한 두 개의 표준 장치가 있다.
Refresh token rotation. Public client에는 refresh token을 한 번 쓰면 새 값으로 회전시키는 게 spec 요구사항이다. 한 토큰이 두 번 쓰이면 탈취 신호로 잡고 세션 전체를 무효화하는 보안 운영이 가능해진다.
Step-up authorization. 이미 토큰을 들고 있는데 호출에 권한이 더 필요한 경우, 서버는 403 + insufficient_scope로 응답한다.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
scope="files:read files:write user:profile",
resource_metadata="https://mcp.example.com/.well-known/..."
클라이언트는 추가로 필요한 scope를 알아내 새 토큰을 한 번 더 받는다. 처음부터 모든 권한을 한 번에 달라고 요구하는 안티패턴을 막는 흐름이다 — 최소 권한 원칙(principle of least privilege)을 표준에 박아둔 셈이다.
scope는 [WWW-Authenticate 챌린지에 들어 있는 값을 우선] 쓰고, 없으면 자원 메타데이터의 scopes_supported를 폴백으로 쓴다. 클라이언트가 어떤 scope를 요구할지 자기 판단으로 만들지 않게 흐름이 짜여 있다.
흔한 함정 둘
1. localhost redirect
Client ID Metadata Documents는 phishing 보호에 도움이 되지만, redirect_uri가 localhost인 경우에는 한계가 있다. 공격자가 정상 클라이언트의 메타데이터 URL을 자기 client_id로 쓰고, 자기 머신의 localhost에 콜백을 띄워 인가 코드를 가로챌 수 있다. 사용자 화면에는 정상 클라이언트의 이름이 그대로 보여 시각으로 구분이 어렵다.
대응은 Authorization Server 쪽에 있다. localhost-only redirect는 추가 경고 화면을 띄우거나 다른 attestation을 요구하는 식으로 다른 redirect보다 한 단계 더 의심해서 처리해야 한다.
2. 동의 누락
MCP 프록시 서버처럼 static client_id를 쓰면서 그 뒤로 동적으로 등록된 여러 클라이언트를 끼워두는 패턴이 있다. 이 경우 동적으로 등록된 각 클라이언트가 사용자 동의를 받아야 한다 — 안 받으면 한 사용자 동의로 여러 익명 클라이언트가 prerequisites를 통과하는 confused deputy의 또 다른 버전이 된다.
다음 편에서는 시리즈를 마무리하면서 생태계 전체를 본다. Inspector·Registry·Tasks(experimental) 같은 도구·확장과, 2026 로드맵의 큰 줄기 — transport scalability, agent communication, governance maturation, enterprise readiness — 가 어디로 향하고 있는지 짚는다.




Loading comments...