Skip to content
ioob.dev
Go back

MCP 7편 — 인증과 권한: OAuth 2.1

· 9분 읽기
mcp 시리즈 (7/8)
  1. MCP 1편 — MCP란 무엇인가
  2. MCP 2편 — 서버가 노출하는 세 가지: Tools, Resources, Prompts
  3. MCP 3편 — 클라이언트가 노출하는 세 가지: Sampling, Roots, Elicitation
  4. MCP 4편 — 메시지 흐름: JSON-RPC 2.0과 lifecycle
  5. MCP 5편 — 첫 MCP 서버 만들기: TypeScript SDK와 Inspector
  6. MCP 6편 — Transport: stdio와 Streamable HTTP
  7. MCP 7편 — 인증과 권한: OAuth 2.1
  8. MCP 8편 — 운영, 레지스트리, 2026 로드맵
Table of contents

Table of contents

세 역할 — Resource Server, Authorization Server, Client

OAuth 2.1의 표준 분업이 그대로 매핑된다.

세 역할을 분리해놓으면 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 경로로 메타데이터를 시도한다.

어느 쪽이든 자원 메타데이터가 돌아오고, 그 안에 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 응답

핵심 디테일 셋.

토큰을 누구에게 — 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 — 가 어디로 향하고 있는지 짚는다.

8편: 운영, 레지스트리, 2026 로드맵


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
MCP 6편 — Transport: stdio와 Streamable HTTP
Next Post
MCP 8편 — 운영, 레지스트리, 2026 로드맵