MCP Authentication: OAuth 2.1 기반 인증 제대로 이해하기

MCP Authentication: OAuth 2.1 기반 인증 제대로 이해하기

MCP 서버를 직접 만들어 보신 적 있으신가요? 처음에는 로컬에서 STDIO로 붙여서 쓰다가, 원격으로 배포하려는 순간 고민이 시작되는데요. “아무나 내 MCP 서버를 호출하면 안 되는데, 누가 접근할 수 있는지 어떻게 가리지?”라는 질문이 떠오르거든요 🤔

이 질문에 대한 공식 답변이 바로 MCP 스펙의 Authorization 섹션입니다. 2025년 11월 25일 개정판에서는 OAuth 2.1을 기반으로 여러 RFC를 조합한 인증 방식을 정리했는데요. 얼핏 보면 OAuth 2.1, RFC 7591, RFC 8414, RFC 8707, RFC 9728 같은 문서가 줄줄이 나와서 어디서부터 봐야 할지 막막합니다.

이 글에서는 MCP 인증 스펙이 왜 이렇게 설계되었는지, 어떤 흐름으로 돌아가는지, 구현할 때 놓치면 안 되는 부분이 무엇인지 차근차근 살펴보겠습니다.

MCP 인증은 왜 필요할까요?

MCP 서버는 종종 사용자의 민감한 데이터에 접근합니다. Gmail을 읽거나 GitHub 저장소에 커밋을 남기거나 결제 이력을 조회하는 서버라면, 아무나 호출하도록 열어 둘 수 없겠죠. 그렇다고 사용자에게 비밀번호를 받아서 서버에 저장하는 것도 답이 아닙니다. 이미 OAuth 2.0이 풀어 놓은 문제를 다시 풀 이유가 없으니까요.

MCP 스펙은 이런 맥락을 그대로 이어받아 다음과 같이 정합니다. STDIO 전송을 쓰는 경우에는 이 스펙을 따르지 않고 환경 변수 같은 로컬 자격 증명에 의존합니다. 반면 HTTP 전송을 쓰는 경우에는 이 스펙을 따르는 것이 좋습니다. 인증 자체는 선택 사항이지만, 보호가 필요한 서버라면 선택의 여지가 거의 없는 셈입니다.

중요한 포인트 하나는 MCP가 새로운 인증 프로토콜을 정의하지 않았다는 점입니다. 스펙은 “simplicity를 유지하면서도 보안과 상호 운용성을 확보하기 위해 기존 표준의 부분 집합을 채택한다”고 밝히는데요. 덕분에 이미 OAuth를 알고 있다면 낯선 개념은 몇 가지밖에 없습니다.

세 가지 역할 이해하기

MCP 인증에 등장하는 주체는 셋입니다. 용어를 먼저 맞추고 가면 나머지 내용이 훨씬 수월해집니다.

우선 MCP 서버는 OAuth 2.1의 Resource Server(자원 서버) 역할을 합니다. 보호된 자원에 대한 요청을 받고, access token을 검증해 응답하는 쪽이죠. 다음으로 MCP 클라이언트는 OAuth 2.1의 Client 역할을 맡습니다. 자원 소유자를 대신해 access token을 받아서 MCP 서버에 요청을 보냅니다. 마지막으로 Authorization Server(인가 서버)는 사용자와 상호작용해 access token을 발급합니다. MCP 서버와 한 몸일 수도 있고 완전히 분리된 외부 서비스일 수도 있는데, 스펙은 구현 세부 사항을 일부러 열어 둡니다.

이렇게 역할을 분리하는 이유는 명확합니다. MCP 서버 개발자가 인가 서버까지 직접 운영하지 않아도 되고, 반대로 이미 운영 중인 Auth0·Okta·Keycloak 같은 인가 서버에 MCP 서버를 얹을 수 있기 때문입니다.

전체 인증 흐름 한눈에 보기

구체적인 필드를 보기 전에 전체 그림을 한번 훑어보겠습니다. 클라이언트가 access token 없이 MCP 서버에 요청을 보내면, 서버는 401 Unauthorized를 반환합니다. 이때 WWW-Authenticate 헤더에 자원 메타데이터 URL을 끼워 주는데, 클라이언트는 이 URL을 따라가 인가 서버의 위치를 알아냅니다. 그다음 인가 서버 메타데이터를 가져오고 PKCE 파라미터를 준비해 사용자 브라우저를 열어 인가 요청을 보냅니다. 사용자가 동의하면 콜백으로 authorization code를 받고, 이를 토큰으로 교환해 마침내 MCP 서버에 정식 요청을 보내게 됩니다.

이 흐름을 스펙이 직접 그려 놓은 다이어그램으로 표현하면 다음과 같습니다.

MCP 인증 흐름
Client            MCP Server            Authorization Server
  |-- 요청 (토큰 없음) -->|
  |<-- 401 + WWW-Authenticate --|
  |-- 자원 메타데이터 요청 -->|
  |<-- authorization_servers 포함 응답 --|
  |----------- 인가 서버 메타데이터 요청 ----------->|
  |<----------- OAuth 2.0 또는 OIDC 메타데이터 ------|
  |----------- 인가 요청 (PKCE + resource) --------->|
  |<----------- 리다이렉트로 code 전달 -------------|
  |----------- 토큰 요청 (code + verifier + resource) ->|
  |<----------- access token ------------------------|
  |-- 요청 + Bearer 토큰 -->|
  |<-- 응답 --|

OAuth 2.0 Authorization Code 흐름과 거의 같지만, MCP가 추가한 요소가 두 가지 보입니다. 자원 메타데이터로 인가 서버를 발견하는 단계와, 모든 토큰 요청에 resource 파라미터를 반드시 포함하는 규칙인데요. 왜 이렇게 만들었는지 하나씩 살펴봅시다.

Protected Resource Metadata로 시작하기

MCP 서버는 RFC 9728에 정의된 OAuth 2.0 Protected Resource Metadata를 반드시 구현해야 합니다. 클라이언트가 “이 MCP 서버를 쓰려면 어느 인가 서버로 가야 하나요?”라고 물었을 때 답을 주는 장치인데요.

메타데이터를 알리는 방법은 두 가지입니다. 하나는 401 Unauthorized 응답에 WWW-Authenticate 헤더를 달아 resource_metadata URL을 꽂아 주는 방식이고, 다른 하나는 /.well-known/oauth-protected-resource 같은 고정된 경로에 메타데이터 문서를 올려 두는 방식입니다. 클라이언트는 두 방식을 모두 이해할 수 있어야 하고, 헤더가 있으면 그걸 우선하고 없으면 well-known URI로 내려가야 합니다.

예를 들어 MCP 서버가 다음과 같이 응답한다고 해 봅시다.

401 응답 예시
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

이 응답에는 두 가지 힌트가 담겨 있습니다. 자원 메타데이터가 어디 있는지(resource_metadata), 그리고 이 자원에 접근하려면 어떤 스코프가 필요한지(scope)입니다. 클라이언트는 메타데이터 URL을 GET으로 조회해 authorization_servers 필드를 읽고, 그중 하나를 골라 인가 흐름을 시작하게 됩니다.

scope 힌트가 있는 이유는 최소 권한 원칙 때문인데요. 클라이언트가 쓸데없이 넓은 스코프를 요청하는 일을 줄이자는 취지입니다. 스펙은 “클라이언트는 헤더에 담긴 스코프를 현재 요청을 충족하기 위한 권위 있는 집합으로 취급해야 한다”고 못 박고 있습니다.

인가 서버 메타데이터 찾기

자원 메타데이터에서 인가 서버 URL을 알아냈다면, 그다음은 인가 서버 메타데이터를 가져올 차례입니다. MCP 인가 서버는 RFC 8414(OAuth 2.0 Authorization Server Metadata) 또는 OpenID Connect Discovery 1.0 중 적어도 하나는 지원해야 하고, 클라이언트는 둘 다 시도할 수 있어야 합니다.

경로 규칙이 살짝 까다로운데요. 인가 서버 URL에 경로가 있는지 없는지에 따라 well-known 경로가 달라지기 때문입니다. 예컨대 https://auth.example.com/tenant1처럼 경로가 있다면 클라이언트는 다음 순서대로 엔드포인트를 시도해야 합니다.

  • https://auth.example.com/.well-known/oauth-authorization-server/tenant1
  • https://auth.example.com/.well-known/openid-configuration/tenant1
  • https://auth.example.com/tenant1/.well-known/openid-configuration

반대로 https://auth.example.com처럼 경로가 없다면 다음 두 가지면 충분합니다.

  • https://auth.example.com/.well-known/oauth-authorization-server
  • https://auth.example.com/.well-known/openid-configuration

이렇게 여러 경로를 시도하는 이유는 OAuth 진영과 OpenID Connect 진영이 서로 다른 관례를 써 왔기 때문입니다. MCP 클라이언트는 양쪽 생태계와 모두 잘 어울려야 하므로 번거롭더라도 모두 확인해야 합니다.

클라이언트 등록의 세 갈래

전통적인 OAuth에서는 사용할 애플리케이션을 인가 서버에 미리 등록해 client_id를 받아 두는 것이 보통인데요. MCP 생태계에서는 클라이언트와 서버가 서로를 모르는 상태에서 만나는 일이 흔합니다. 그래서 스펙은 세 가지 등록 방식을 함께 제시합니다.

우선 Pre-registration(사전 등록)은 기존 OAuth와 같습니다. 이미 관계가 있는 쌍이라면 정적인 client_id와 credentials를 쓰면 됩니다. 가장 단순하지만 관계가 없는 낯선 서버에는 쓸 수 없습니다.

다음으로 Dynamic Client Registration(RFC 7591)은 클라이언트가 인가 서버의 /register 엔드포인트에 POST를 보내 그 자리에서 client_id를 발급받는 방식입니다. 사용자 개입 없이 자동화하기 좋지만, 무분별한 등록을 막으려면 서버 쪽에서 추가 정책을 얹어야 합니다. 이 방식은 예전 MCP 인증 스펙에서 권장되던 방법이라 호환성을 위해 남아 있습니다.

마지막으로 가장 새로운 Client ID Metadata Documents(CIMD)는 클라이언트가 자기 메타데이터를 HTTPS URL에 올려 두고, 그 URL 자체를 client_id로 쓰는 방식입니다. 예를 들어 https://app.example.com/oauth/client-metadata.json이 client_id가 되는데요. 인가 서버는 URL 형태의 client_id를 만나면 해당 문서를 가져와 검증합니다.

메타데이터 문서는 대략 이렇게 생겼습니다.

client-metadata.json
{
  "client_id": "https://app.example.com/oauth/client-metadata.json",
  "client_name": "Example MCP Client",
  "client_uri": "https://app.example.com",
  "logo_uri": "https://app.example.com/logo.png",
  "redirect_uris": [
    "http://127.0.0.1:3000/callback",
    "http://localhost:3000/callback"
  ],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

client_id 값이 문서가 호스팅된 URL과 정확히 일치해야 한다는 점이 핵심인데요. 이 규칙 덕분에 인가 서버는 클라이언트를 한 번도 본 적 없어도 신뢰할 수 있는 고정된 식별자를 확보하게 됩니다. 스펙은 클라이언트가 모든 방식을 지원한다면 사전 등록 → CIMD → Dynamic Client Registration → 사용자 입력 순으로 시도하라고 권장합니다.

PKCE는 선택이 아닌 필수

MCP 클라이언트는 OAuth 2.1 Section 7.5.2에 따라 PKCE(Proof Key for Code Exchange)를 반드시 구현해야 하고, 인가 서버의 PKCE 지원 여부를 확인한 후에만 흐름을 진행해야 합니다.

PKCE는 authorization code 탈취와 주입 공격을 막아 주는 장치인데요. 클라이언트가 code_verifier라는 무작위 문자열을 만들어서 해시한 code_challenge를 먼저 인가 요청에 담아 보내고, 나중에 토큰 교환 단계에서 원본 code_verifier를 함께 제출합니다. 인가 서버는 둘을 맞춰 보고 일치할 때만 토큰을 내줍니다. authorization code를 중간에서 가로챈 공격자라도 code_verifier는 알 수 없으므로 토큰 교환에 실패하게 됩니다. 동작 원리와 직접 구현 예제가 궁금하다면 PKCE로 Authorization Code 흐름 안전하게 보호하기를 참고해 보세요.

스펙은 여기서 한 수 더 둡니다. 클라이언트는 기술적으로 가능하면 반드시 S256 방식을 써야 하고, 인가 서버 메타데이터에 code_challenge_methods_supported 필드가 없으면 진행을 거부해야 합니다. “설마 PKCE 안 하는 서버는 없겠지”라고 낙관하는 대신, 메타데이터에서 명시적으로 확인되지 않으면 중단하라는 단호한 규칙인 셈이죠.

Resource Indicator로 토큰 오용 막기

개인적으로 MCP 인증 스펙에서 가장 흥미롭게 본 부분이 바로 RFC 8707 Resource Indicators 의무화입니다. 클라이언트는 인가 요청과 토큰 요청 양쪽에 resource 파라미터를 반드시 넣어야 하고, 그 값은 토큰을 사용할 MCP 서버의 정규 URI여야 합니다.

정규 URI의 조건은 꽤 깐깐합니다. 스킴과 호스트는 소문자로, 프래그먼트는 없이, 가능한 한 구체적인 형태를 써야 합니다. https://mcp.example.com/mcp, https://mcp.example.com:8443 같은 값은 유효하지만 mcp.example.com(스킴 누락)이나 https://mcp.example.com#fragment(프래그먼트 포함)는 안 됩니다.

이렇게 강제하는 이유는 Confused Deputy 문제 때문입니다. 만약 서버 A용으로 발급된 토큰을 공격자가 빼내서 서버 B에 들이밀었을 때, 서버 B가 토큰을 덜컥 받아 주면 권한 경계가 무너지겠죠. resource 파라미터로 토큰을 특정 자원에 묶어 두면, MCP 서버가 “이 토큰은 나한테 발급된 게 아니네”라며 거절할 수 있게 됩니다.

스펙은 이 결함을 두 각도에서 짚고 넘어가는데요. audience 검증 실패(내가 받을 토큰이 아닌데 받아 주는 것)와 token passthrough(받은 토큰을 그대로 상류 API에 넘기는 것) 모두 금지됩니다. MCP 서버가 상류 API를 호출해야 한다면 별개의 OAuth 클라이언트 흐름으로 새 토큰을 발급받아야 합니다.

Bearer 토큰 사용과 오류 처리

토큰이 일단 손에 들어오면 사용법은 OAuth 2.1과 동일합니다. 모든 요청에 Authorization: Bearer <토큰> 헤더를 실어 보내면 됩니다. URI 쿼리 문자열에 토큰을 넣는 방식은 금지되어 있습니다. 로그에 남기 쉽고, 브라우저 히스토리와 리퍼러로 새어 나갈 위험이 있기 때문이죠.

MCP 요청 예시
GET /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

서버 쪽 오류 응답은 세 가지 상태 코드로 정리됩니다. 401 Unauthorized는 인가가 필요하거나 토큰이 유효하지 않은 상황이고, 403 Forbidden은 토큰은 맞지만 스코프가 부족한 상황, 400 Bad Request는 인가 요청 자체가 잘못된 경우입니다.

여기서 403401을 헷갈리지 않아야 합니다. 스펙은 인가 실패와 권한 부족을 분명히 구분합니다. 클라이언트 입장에서도 대처가 달라지는데요. 401이면 재인증부터 시작해야 하지만, 403이면 기존 토큰을 버리지 않고 스코프만 더 요청하면 됩니다.

스코프 상승 흐름

이미 토큰을 쥐고 있는데 특정 기능에 대한 스코프가 부족할 때를 위해, 스펙은 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/oauth-protected-resource",
                         error_description="Additional file write permission required"

클라이언트는 이 응답을 보고 스코프 목록을 파싱해 재인가 흐름을 돌린 뒤, 새로 받은 토큰으로 원래 요청을 다시 시도합니다. 이때 스펙은 무한 재시도를 경계합니다. 같은 자원·동작 조합에 대해 몇 번 이상 실패하면 영구 실패로 간주하고 멈추라고 권고하는데요. 사용자가 동의 화면을 끝없이 보게 만드는 재앙을 막자는 취지입니다.

서버 쪽도 스코프를 어떻게 돌려줄지 생각해야 합니다. 스펙은 “이번 요청에 필요한 최소 스코프만 주는 방식”과 “기존 스코프에 새 스코프를 보태서 주는 방식” 중 권장 안으로 후자를 꼽습니다. 기존 권한을 잃지 않도록 하면서도 마찰을 줄이자는 실용적인 지침입니다.

보안상 놓치면 안 되는 포인트

스펙의 보안 섹션은 생각보다 깁니다. 몇 가지 핵심만 추려 보겠습니다.

우선 모든 인가 서버 엔드포인트는 HTTPS로 제공되어야 하고, 리다이렉트 URI도 localhost가 아닌 이상 HTTPS여야 합니다. Access token은 수명을 짧게 두고 Refresh token은 공개 클라이언트라면 반드시 회전(rotate)시켜야 합니다. 토큰이 유출돼도 피해 범위를 최소화하려는 조치입니다.

Open Redirection 공격을 막기 위해 리다이렉트 URI는 사전 등록된 값과 정확히 일치하는지 검증해야 합니다. 부분 문자열 매칭이나 와일드카드는 금물인데요. 국가급 공격자가 아니더라도 URL 파싱 허점 하나로 세션을 가로챌 수 있습니다. 클라이언트는 state 파라미터를 만들어 인가 요청과 콜백 사이에 일치하는지 반드시 확인해야 합니다.

Client ID Metadata Documents 방식을 쓸 때는 한 가지 더 조심할 부분이 있습니다. 공격자가 합법적인 클라이언트의 메타데이터 URL을 가져다 자기 client_id로 쓰면서, 자기가 열어 놓은 로컬호스트 포트를 redirect_uri로 지정하는 공격이 가능합니다. 이 경우 사용자는 합법적인 클라이언트 이름과 로고를 보면서 실제로는 공격자에게 코드를 넘기게 됩니다. 인가 서버는 로컬호스트 리다이렉트에 대해 경고를 추가로 표시하거나 추가 증명 수단을 요구하는 쪽으로 보완해야 합니다.

마지막으로 Confused Deputy. MCP 서버가 상류 API로 가는 프록시 역할을 할 때, 정적인 client_id를 재사용하면 공격자가 훔친 authorization code로 다른 사용자를 사칭할 수 있습니다. 스펙은 이런 프록시 서버가 동적으로 등록된 각 클라이언트에 대해 상류 인가 서버로 포워딩하기 전에 사용자 동의를 별도로 받아야 한다고 규정합니다.

마치며

MCP 인증 스펙을 한마디로 요약하면 “OAuth 2.1에 RFC 9728과 RFC 8707을 단단히 결합한 것”입니다. 새로운 프로토콜을 배우는 부담은 적지만, 의무 사항이 꽤 많아서 제대로 구현하려면 세부 조항을 꼼꼼히 확인해야 합니다.

구현을 시작한다면 순서를 이렇게 잡아 보시길 권합니다. Protected Resource Metadata부터 올려서 401 + WWW-Authenticate 응답을 정리하고, 그다음 PKCE와 resource 파라미터가 올바르게 흘러가는지 확인하고, 마지막으로 스코프 상승 흐름까지 맞춰 보는 식이죠. MCP Inspector로 요청·응답을 들여다보면서 한 단계씩 검증해 나가면 막막함이 많이 줄어듭니다. 이미 배포된 MCP 서버가 궁금하다면 MCP Registry에서 실제 구현 사례를 살펴볼 수도 있고요.

MCP 생태계가 빠르게 성숙하면서 인증 스펙도 계속 다듬어지고 있는데요. 더 자세한 규정과 최신 변경 사항은 MCP Authorization 공식 스펙을 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord