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 서버가 직접 로그인·토큰 발급까지 하면 보안 표면이 커지고, 기업의 중앙 집중식 보안과 안 맞습니다. 서버를 "토큰 검증 + 내부 권한(RBAC) 집행"으로 한정하면, 검증된 IdP 생태계를 재사용하고 서버를 무상태에 가깝게 유지해 확장·운영이 쉬워집니다(08장의 stateless와 통함).

표준 빌딩블록

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가 됨
💡 팁
DCR → CIMD 흐름 변화: 초기엔 Dynamic Client Registration(RFC 7591, 런타임 클라이언트 등록)이 권장이었으나, 2025-11-25에서 CIMD가 권장 기본이 됐습니다. DCR도 여전히 쓰이고, 많은 기업은 신뢰 클라이언트를 사전 등록해 우회합니다. 어느 메커니즘을 쓸지는 사용하는 IdP 지원에 달려 있으니 확인하세요.

인증 흐름 — 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: ⑨ 정상 응답
  1. 클라이언트가 토큰 없이 보호된 자원을 요청합니다.
  2. 서버가 401WWW-Authenticate: Bearer resource_metadata="..." 를 돌려줍니다.
  3. 클라이언트가 그 URL(또는 well-known /.well-known/oauth-protected-resource)에서 PRM 문서를 가져옵니다.
  4. PRM의 authorization_servers로 어느 AS에서 토큰을 받을지 알아냅니다.
  5. AS 메타데이터로 엔드포인트를 발견하고, PKCE 인가 코드 흐름을 수행합니다. 이때 resource 파라미터(RFC 8707)로 이 서버를 위한 토큰임을 명시합니다.
  6. AS가 aud(audience)에 서버 URI를 담은 액세스 토큰을 발급합니다.
  7. 클라이언트가 Authorization: Bearer <토큰>으로 재요청합니다.
  8. 서버가 토큰을 검증합니다.

PRM 문서는 대략 이런 모양입니다(서버가 /.well-known/oauth-protected-resource로 게시):

JSON
{
  "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에서 명시적 보안 지침으로 강화됐습니다.
💡 팁
현실 주의: 일부 IdP(예: 특정 버전 Keycloak)는 RFC 8707을 완전히 지원하지 않아, 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으로 스코프를 점증한다.

✍️ 확인 문제

  1. MCP 서버가 "Authorization Server가 아니라 Resource Server"라는 말의 실제 의미는? 서버가 하지 않는 일을 하나 들어 보자.
  2. 클라이언트가 토큰 없이 호출했을 때 서버는 무엇을 돌려줘야 클라이언트가 인증 방법을 발견할 수 있나?
  3. MCP 서버가 받은 토큰을 업스트림 GitHub API에 그대로 넘기면 어떤 공격에 노출되나? 올바른 처리는?
다음 장: 20. 권한·동의·human-in-the-loop