19. 원격 서버 인증 (OAuth 2.1 기반)
- MCP 서버가 "Resource Server"이지 "Authorization Server"가 아니라는 분리를 이해한다
- 401 → Protected Resource Metadata → 토큰 발급 → 검증의 흐름을 그릴 수 있다
- 토큰 audience 검증과 "토큰 패스스루 금지"가 왜 보안의 핵심인지 안다
7부는 보안을 1급 주제로 다룹니다. 첫 장은 인증 — 원격 서버가 "누가 부르는지"를 확인하는 방법입니다.
먼저: stdio는 이 장의 대상이 아니다
MCP 인증 스펙은 HTTP 기반 전송을 위한 것입니다. stdio 전송 구현은 이 스펙을 따르지 않고, 대신 환경에서 자격(credential)을 가져옵니다(07장·14장에서 본 환경 변수 주입). 인증은 MCP에서 선택(OPTIONAL) 이지만, 원격(HTTP) 서버를 공개한다면 사실상 필수입니다.
flowchart TD
Q{"전송 방식?"}
Q -->|"stdio (로컬)"| ENV["환경 변수에서 자격 취득\n(OAuth 스펙 미적용)"]
Q -->|"HTTP (원격)"| OAUTH["OAuth 2.1 기반 인증\n(이 장의 내용)"]
classDef q fill:#bfdbfe,stroke:#1d4ed8,color:#000;
classDef env fill:#5eead4,stroke:#0f766e,color:#000;
classDef oauth fill:#fde68a,stroke:#b45309,color:#000;
class Q q;
class ENV env;
class OAUTH oauth;
먼저: OAuth와 관련 용어 풀이
이 장에는 보안·인증 약어가 쏟아집니다. 본론 전에 한 번에 풀어 둡니다.
- OAuth 2.0 / 2.1: "비밀번호를 직접 넘기지 않고, 제3자 앱에 제한된 권한을 위임"하는 업계 표준 인증·인가 프레임워크입니다. "구글로 로그인" 버튼 뒤에서 도는 게 바로 이것입니다. 2.1은 그동안의 모범 사례를 모아 정리한 최신 정비판입니다.
- IdP (Identity Provider, 신원 제공자): 사용자의 로그인을 처리하고 토큰을 발급하는 주체입니다. Auth0·Okta·Keycloak·Azure AD 같은 서비스가 IdP입니다.
- 액세스 토큰(access token): 로그인 후 발급되는 "출입증"입니다. 이후 요청에 이 토큰을 첨부하면, 서버는 비밀번호를 다시 묻지 않고 토큰만 검증합니다(08장의 Bearer 토큰).
- PKCE (Proof Key for Code Exchange, "픽시"로 읽음): 인증 과정에서 오가는 인가 코드를 중간에 누가 가로채도 악용하지 못하게 막는 보강 장치입니다. 모바일·CLI처럼 비밀을 숨기기 어려운 클라이언트가 많아 OAuth 2.1에서 필수가 됐습니다.
- RFC: Request For Comments. 인터넷 표준을 정의하는 공개 문서 번호입니다(예: RFC 9728). "RFC 9728을 따른다"는 곧 "그 표준 문서가 정한 규칙대로 한다"는 뜻입니다.
- RBAC (Role-Based Access Control): 권한을 역할 단위로 묶어 관리하는 방식입니다(예: "관리자" 역할은 삭제 가능, "뷰어" 역할은 읽기만). 서버가 내부적으로 "이 사용자가 이 작업을 해도 되는지" 판단할 때 씁니다.
핵심 분리 — 서버는 Resource Server다
MCP 인증의 가장 중요한 설계 결정은 역할 분리입니다. 2025-06 리비전에서 명문화됐고 2025-11-25에서 확립됐습니다.
- MCP 서버 = OAuth 2.1 Resource Server: 외부 인증 서버가 발급한 액세스 토큰을 검증만 합니다. 로그인을 관리하거나 토큰을 발급하지 않습니다.
- Authorization Server(AS) = 별도의 IdP: 기업의 기존 IdP(Auth0·Okta·Keycloak·Azure AD 등)가 사용자 로그인과 토큰 발급을 담당합니다.
flowchart LR
CLIENT["MCP Client"] -->|"① 토큰 요청"| AS["인증 서버 (IdP)\nAuth0/Okta/...\n= Authorization Server"]
AS -->|"② 액세스 토큰"| CLIENT
CLIENT -->|"③ 토큰 첨부해 호출"| MCP["MCP Server\n= Resource Server\n(토큰 검증만)"]
classDef client fill:#fde68a,stroke:#b45309,color:#000;
classDef as fill:#ddd6fe,stroke:#6d28d9,color:#000;
classDef rs fill:#5eead4,stroke:#0f766e,color:#000;
class CLIENT client;
class AS as;
class MCP rs;
표준 빌딩블록
MCP 인증은 OAuth 2.1 위에, 몇 가지 RFC를 골라 얹습니다.
| 표준 | 역할 |
|---|---|
| OAuth 2.1 | 기반 프레임워크. PKCE 필수(공개 클라이언트가 많아 코드 가로채기 방어) |
| RFC 9728 (Protected Resource Metadata) | 서버가 "어디서 인증받는지"를 광고. MCP 서버 MUST 구현 |
| RFC 8707 (Resource Indicators) | 토큰을 특정 서버에 묶음(audience 바인딩). 토큰 재사용 방지 |
| RFC 8414 (Authorization Server Metadata) | 클라이언트가 인증 엔드포인트를 자동 발견 |
| CIMD (Client ID Metadata Documents) | 2025-11-25에서 DCR을 대체해 권장 기본으로. 클라이언트가 HTTPS URL에 정적 메타데이터를 게시하고 그 URL이 client ID가 됨 |
인증 흐름 — 401에서 시작하는 부트스트랩
에이전트는 어떤 API를 부를지 런타임에 정해지므로, "어디서 토큰을 받는지"를 미리 모를 수 있습니다. RFC 9728이 이 문제를 풉니다 — 서버가 401과 함께 "여기서 인증받아라"를 알려 주는 것.
sequenceDiagram
participant C as MCP Client
participant RS as MCP Server (Resource Server)
participant AS as Authorization Server (IdP)
C->>RS: ① 토큰 없이 요청
RS-->>C: ② 401 + WWW-Authenticate(resource_metadata=...)
C->>RS: ③ GET /.well-known/oauth-protected-resource
RS-->>C: ④ PRM 문서 (authorization_servers, scopes_supported)
C->>AS: ⑤ AS 메타데이터 조회 → PKCE 인가 코드 흐름 (resource=서버 URI)
AS-->>C: ⑥ 액세스 토큰 (aud = 서버 URI)
C->>RS: ⑦ Authorization: Bearer <토큰> 으로 재요청
RS->>RS: ⑧ 토큰 검증 (서명·만료·audience)
RS-->>C: ⑨ 정상 응답
- 클라이언트가 토큰 없이 보호된 자원을 요청합니다.
- 서버가 401과
WWW-Authenticate: Bearer resource_metadata="..."를 돌려줍니다. - 클라이언트가 그 URL(또는 well-known
/.well-known/oauth-protected-resource)에서 PRM 문서를 가져옵니다. - PRM의
authorization_servers로 어느 AS에서 토큰을 받을지 알아냅니다. - AS 메타데이터로 엔드포인트를 발견하고, PKCE 인가 코드 흐름을 수행합니다. 이때
resource파라미터(RFC 8707)로 이 서버를 위한 토큰임을 명시합니다. - AS가
aud(audience)에 서버 URI를 담은 액세스 토큰을 발급합니다. - 클라이언트가
Authorization: Bearer <토큰>으로 재요청합니다. - 서버가 토큰을 검증합니다.
PRM 문서는 대략 이런 모양입니다(서버가 /.well-known/oauth-protected-resource로 게시):
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://idp.example.com"],
"scopes_supported": ["mcp:read", "mcp:tools"],
"bearer_methods_supported": ["header"]
}
/.well-known/oauth-protected-resource 경로는 고정입니다(RFC 9728). 임의로 바꾸면 클라이언트가 발견하지 못합니다. 또 6-2025 리비전부터 옛 기본 엔드포인트(/authorize 등) 폴백이 제거되고 PRM이 필수가 됐으니, 옛 튜토리얼의 폴백 가정을 쓰지 마세요.토큰 검증 — 서버의 핵심 책임
서버는 Resource Server로서 OAuth 2.1 Section 5.2대로 토큰을 검증해야 합니다(MUST). 특히:
- 서명·만료 검증.
- audience(
aud) 검증: 토큰이 바로 이 서버를 위해 발급됐는지 RFC 8707에 따라 확인. 서버는 자기 정규 URI(canonical URI)가 토큰의aud에 들어 있는지 봐야 합니다. - 실패 시 만료·무효 토큰은 401, 스코프 부족은 403 +
WWW-Authenticate(error="insufficient_scope", 필요한 scope 명시)로 응답.
🔒 audience 검증은 타협 불가: 클라이언트는 MCP 서버의 인증 서버가 발급한 토큰 외에는 보내면 안 되고(MUST NOT), 서버는 자기 자원에 유효한 토큰만 받아야 합니다(MUST). audience 검증을 빼먹으면, 다른 서비스용 토큰이 이 서버에서 통용되는 사고가 납니다. 이는 실제 공개된 취약점들의 핵심이었고, 2025-11-25에서 명시적 보안 지침으로 강화됐습니다.
aud 클레임이 비거나 다르게 채워질 수 있습니다. 그럴 땐 Audience Mapper로 aud를 채우는 식의 우회가 필요합니다. 사용하는 IdP의 실제 동작을 확인하세요.토큰 패스스루 금지 (Confused Deputy 예방)
서버가 업스트림 API를 호출해야 하면(예: MCP 서버가 GitHub API를 부름), 클라이언트에게 받은 토큰을 그대로 흘려보내면 안 됩니다(MUST NOT). 서버는 그 업스트림에 대해 별도의 OAuth 클라이언트가 되어 자기 토큰을 따로 얻어야 합니다.
flowchart LR
C["Client"] -->|"토큰 A (aud=MCP서버)"| MCP["MCP Server"]
MCP -.->|"❌ 토큰 A 패스스루"| UP["Upstream API"]
MCP -->|"✅ 별도 토큰 B 획득"| UP
classDef c fill:#fde68a,stroke:#b45309,color:#000;
classDef m fill:#5eead4,stroke:#0f766e,color:#000;
classDef bad fill:#fca5a5,stroke:#b91c1c,color:#000;
class C c;
class MCP m;
class UP bad;
🔒 왜 위험한가 — 혼동된 대리인(confused deputy): 클라이언트용으로 발급된 토큰을 업스트림에 그대로 넘기면, 업스트림이 "MCP 서버를 신뢰하니 이 토큰도 신뢰"하게 되어, 의도치 않은 권한이 새 나갈 수 있습니다. 2025-06 스펙은 이 패스스루를 명시적으로 금지합니다. (공격 자체는 21장에서 더 다룹니다.)
최소 권한과 step-up 인증
처음부터 넓은 권한을 요구하지 말고, 현재 작업에 필요한 최소 스코프만 요청하세요(최소 권한 원칙). 나중에 더 필요하면 step-up authorization으로 스코프를 확장합니다. 2025-11-25은 증분(incremental) 스코프 동의를 강화했습니다. 서버는 스코프 부족 시 403 + 필요한 scope를 알려, 클라이언트가 추가 동의를 받게 합니다.
이 장에서 배운 것
- 인증은 HTTP 전송의 주제(stdio는 환경 변수). MCP 서버는 Resource Server일 뿐, 토큰 발급은 외부 IdP(AS) 가 한다.
- 흐름: 401 + WWW-Authenticate → PRM(RFC 9728) → AS에서 PKCE로 토큰(RFC 8707 resource) → Bearer로 재요청 → 검증.
- 서버는 audience를 반드시 검증한다(자기 토큰만 수락). 만료/무효=401, 스코프 부족=403.
- 토큰 패스스루 금지 — 업스트림 호출엔 별도 토큰. 최소 권한 + step-up으로 스코프를 점증한다.
✍️ 확인 문제
- MCP 서버가 "Authorization Server가 아니라 Resource Server"라는 말의 실제 의미는? 서버가 하지 않는 일을 하나 들어 보자.
- 클라이언트가 토큰 없이 호출했을 때 서버는 무엇을 돌려줘야 클라이언트가 인증 방법을 발견할 수 있나?
- MCP 서버가 받은 토큰을 업스트림 GitHub API에 그대로 넘기면 어떤 공격에 노출되나? 올바른 처리는?
다음 장: 20. 권한·동의·human-in-the-loop