프로덕션 MCP Auth — iii 프리미티브 위의 DCR, JWKS 회전, Audience 고정 토큰

Lesson 16에서는 OAuth 2.1 상태 머신을 메모리 위에 세웠습니다. 2026년 기준으로 실제 조직에 배포하는 모든 MCP 서버는 프로덕션(production) 수준의 인증/인가(auth) 뒤에 놓입니다. 여기에는 동적 클라이언트 등록(Dynamic Client Registration; DCR, RFC 7591), 인가 서버 메타데이터 발견(Authorization Server Metadata Discovery, RFC 8414), 새벽 3시의 토큰 검증을 깨뜨리지 않는 JWKS 회전(rotation), 그리고 혼동된 대리인(confused-deputy) 재사용을 거부하는 청중(audience) 고정 토큰이 포함됩니다. 이 lesson에서는 이 모두를 iii 프리미티브(primitive)로 엮어냅니다. HTTP와 cron에는 iii.registerTrigger를, 인증/인가 로직에는 iii.registerFunction을, 캐시된 키에는 state::set/get을 사용합니다. 그렇게 구성하면 인증/인가 표면이 엔진(engine)의 다른 워크로드(workload)처럼 관찰 가능(observable)하고, 재시작 가능(restartable)하며, 재생 가능(replayable)한 상태가 됩니다.

유형: Build
언어: Python (표준 라이브러리, lesson 환경을 위한 iii 프리미티브 mock)
선수 학습: Phase 13 · 16 (OAuth 2.1 상태 머신), Phase 13 · 17 (게이트웨이)
소요 시간: 약 90분

학습 목표

  • RFC 8414 메타데이터(metadata)로 인가 서버를 발견하고 계약(contract)을 검증할 수 있습니다.
  • RFC 7591 동적 클라이언트 등록을 구현해 MCP 클라이언트가 관리자 개입 없이 스스로 등록되게 합니다.
  • cron 트리거(trigger)로 JWKS 키를 캐시하고 회전시켜, 키 교체 시점(key roll-over) 중에도 서명 검증이 계속 살아남게 합니다.
  • RFC 8707 리소스 지시자(resource indicator)를 사용해 토큰을 단일 MCP 리소스에 고정하고, 혼동된 대리인(confused-deputy) 재사용을 거부합니다.
  • 모든 엔드포인트(endpoint)와 백그라운드(background) 작업을 iii 프리미티브로 연결합니다. HTTP 트리거, cron 트리거, 이름이 부여된 함수(function), state::* 읽기를 사용해 한 번의 재시작만으로 인증/인가 표면이 재구성되게 합니다.
  • IdP 역량 매트릭스(IdP capability matrix)를 읽고, 해당 IdP가 MCP 인증/인가 프로파일(profile)을 만족하지 못하면 배포를 거부할 수 있어야 합니다.

문제

Lesson 16의 시뮬레이터(simulator)는 OAuth 2.1을 메모리 안에서 실행합니다. 그러나 프로덕션 환경에는 메모리 전용 시뮬레이터가 결코 보지 못하는 운영상의 공백이 세 가지 존재합니다.

첫 번째 공백은 등록(enrollment)입니다. 실제 조직에서는 수백 개의 MCP 서버와 수천 개의 MCP 클라이언트가 동시에 운영됩니다. 운영자가 모든 Cursor 사용자를 OAuth 클라이언트로 손수 등록하는 일은 현실적으로 불가능합니다. RFC 7591의 동적 클라이언트 등록은 클라이언트가 인가 서버에 POST /register를 보내고 그 자리에서 client_id와, 필요하다면 client_secret까지 받게 해 줍니다. 서버는 RFC 8414 메타데이터에 registration_endpoint를 게시하고, 클라이언트는 별도 채널(out-of-band) 설정 없이 이를 자동으로 발견합니다.

두 번째 공백은 키 회전(key rotation)입니다. JWT 검증은 인가 서버의 서명 키(signing key)에 의존하며, 이 키는 JSON Web Key Set(JWKS)으로 게시됩니다. 인가 서버는 보통 정기적으로(자주는 매시간 단위로), 사고 대응 중에는 그보다 빠른 주기로 키를 회전시킵니다. MCP 서버가 부팅 시점에 JWKS를 단 한 번만 가져온다면, 회전이 일어나는 시점까지는 잘 검증하다가 그 이후의 모든 요청이 서버를 재시작하기 전까지 실패하게 됩니다. 프로덕션에서는 JWKS를 캐시된 값으로 연결해 두고, 이전 키가 만료되기 전에 캐시를 덮어쓰는 새로고침 작업(refresh job)을 함께 둡니다. 그리고 캐시보다 더 새로운 키로 서명된 토큰이 도착하는 경우를 위해 캐시 미스(cache miss) 시 대체 가져오기(fallback fetch)를 추가로 둡니다.

세 번째 공백은 청중 결속(audience binding)입니다. Lesson 16에서는 RFC 8707 리소스 지시자(resource indicator)를 소개했습니다. 프로덕션에서는 이 지시자가 모든 요청에 대해 강력한 클레임 검사(claim check)로 작동합니다. MCP 서버는 token.aud를 자신의 표준(canonical) 리소스 URL과 비교하고, 일치하지 않으면 HTTP 401로 거부합니다. 이는 상류(upstream) MCP 서버나 한 서버용으로 발급된 토큰을 보유한 악의적인 클라이언트가 같은 신뢰 망(trust mesh) 안의 다른 서버에 그 토큰을 재생(replay)하는 공격을 막는 유일한 방어 수단입니다.

이 lesson에서는 이러한 모든 공백을 iii 프리미티브로 풀어냅니다. 메타데이터 문서는 어떤 함수의 출력을 반환하는 HTTP 트리거이고, JWKS 회전은 auth::rotate-jwks를 호출하는 cron 트리거이며, 이 함수는 state::set("auth/jwks/<issuer>", ...)에 결과를 기록합니다. JWT 검증은 다른 함수가 iii.trigger("auth::validate-jwt", token) 형태로 호출하는 함수입니다. MCP 서버 자체도 결국에는 디스패치(dispatch) 전에 검증을 호출하는 또 하나의 HTTP 트리거일 뿐입니다. 엔진을 재시작하면 트리거 레지스트리(trigger registry)는 다시 구축되고, 상태(state)는 그대로 살아남으며, 인증/인가 표면 전체가 수동 조정 없이 운영 상태로 복귀합니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.프로덕션 MCP 인증가 해결하는 핵심 과제는?

2.프로덕션 MCP 인증 이전의 주요 한계는?

0/2 답변 완료

개념

RFC 8414 — OAuth Authorization Server Metadata

/.well-known/oauth-authorization-server의 문서는 클라이언트가 필요한 모든 것을 설명합니다.

{
  "issuer": "https://auth.example.com",
  "authorization_endpoint": "https://auth.example.com/authorize",
  "token_endpoint": "https://auth.example.com/token",
  "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
  "registration_endpoint": "https://auth.example.com/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["mcp:tools.read", "mcp:tools.invoke"],
  "token_endpoint_auth_methods_supported": ["none", "private_key_jwt"]
}

MCP 리소스 URL을 부여받은 클라이언트는 발견(discovery)을 연쇄적으로 이어 갑니다. 먼저 RFC 9728의 oauth-protected-resource, 즉 리소스 서버의 문서가 발급자(issuer)를 명시하고, 이어서 RFC 8414의 oauth-authorization-server가 모든 엔드포인트를 명시합니다. 클라이언트는 인가 URL(authorization URL)을 결코 하드코딩하지 않습니다.

MCP를 위해 어떤 IdP를 신뢰하기 전에, 다음과 같은 계약을 반드시 검증해야 합니다.

  • code_challenge_methods_supportedS256이 포함되어야 합니다(RFC 7636에 정의된 PKCE).
  • grant_types_supportedauthorization_code가 포함되어야 하며, passwordimplicit는 거부되어야 합니다.
  • registration_endpoint가 존재해야 합니다(RFC 7591 지원 여부).
  • OAuth 2.1 환경에서는 response_types_supported가 정확히 ["code"]여야 합니다.

이 중 하나라도 누락되면 MCP 서버는 해당 IdP에 대한 배포를 거부합니다. 잘못된 것은 코드가 아니라 배포 매니페스트(deployment manifest) 쪽이라고 봐야 합니다.

RFC 9728 복습 — 보호된 리소스 메타데이터(Protected Resource Metadata)

Lesson 16에서 RFC 9728을 다뤘습니다. 프로덕션에서의 차이점은, 이 문서가 클라이언트 입장에서 MCP 서버가 신뢰하는 인가 서버를 찾을 수 있는 유일한 장소라는 점입니다. 하나의 MCP 서버가 여러 IdP의 토큰을 함께 받아들이는 경우도 있습니다. 예컨대 하나는 내부 직원용, 다른 하나는 외부 파트너용일 수 있습니다. RFC 9728은 그 집합을 선언하는 역할을 하고, RFC 8414는 각 IdP가 무엇을 지원하는지 문서화합니다.

{
  "resource": "https://notes.example.com",
  "authorization_servers": ["https://auth.example.com", "https://partners.example.com"],
  "scopes_supported": ["mcp:tools.invoke"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://notes.example.com/docs"
}

RFC 7591 — 동적 클라이언트 등록(Dynamic Client Registration)

DCR이 없는 환경에서는 모든 MCP 클라이언트(Cursor, Claude Desktop, 직접 만든 에이전트 등)가 IdP 관리자와 별도 채널(out-of-band)로 교환을 거쳐야만 합니다. 반면 DCR이 있으면 클라이언트는 다음과 같은 요청을 직접 POST합니다.

POST /register
Content-Type: application/json

{
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp:tools.invoke",
  "client_name": "Cursor",
  "software_id": "com.cursor.cursor",
  "software_version": "0.42.0"
}

서버는 이후 업데이트에 사용할 registration_access_token과 함께 client_id를 응답으로 돌려줍니다.

{
  "client_id": "c_3e7f1a",
  "client_id_issued_at": 1769472000,
  "redirect_uris": ["http://127.0.0.1:7333/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "registration_access_token": "regt_b2...",
  "registration_client_uri": "https://auth.example.com/register/c_3e7f1a"
}

token_endpoint_auth_method: none은 사용자 기기 위에서 실행되는 MCP 클라이언트에게 적절한 기본값입니다. 이들은 오직 client_id만 받습니다. 즉 유출될 수 있는 client_secret이 처음부터 존재하지 않습니다. 공개 클라이언트(public client)에 필요한 소유 증명(proof-of-possession)은 PKCE가 대신 제공합니다.

프로덕션에서 흔히 빠지는 함정은 다음 세 가지입니다.

  • 등록 엔드포인트는 원본 IP(source IP)별로 속도 제한(rate limit)을 적용해야 합니다. 그렇지 않으면 적대자가 수백만 개의 가짜 등록을 스크립트로 만들어 client_id 이름공간(namespace)을 고갈시킬 수 있습니다. iii 환경에서는 이를 매우 간단하게 처리할 수 있습니다. 등록 HTTP 트리거가 등록 처리기로 디스패치하기 전에 먼저 auth::rate-limit 함수를 호출하면 됩니다.
  • 일부 엔터프라이즈 IdP는 software_statement, 즉 클라이언트를 보증하는 서명된 JWT를 요구합니다. 이 lesson에서 사용하는 모의 구현(mock)은 이를 생략합니다. 프로덕션에서는 로컬호스트(localhost) 리다이렉트 URI가 아닌 곳에서 들어온 서명 없는 등록을 거부하는 검증 단계를 함께 연결해야 합니다.
  • registration_access_token은 평문이 아니라 해시(hash)된 형태로 저장해야 합니다. 이 토큰이 탈취되면 공격자가 클라이언트의 리다이렉트 URI를 임의로 다시 쓸 수 있게 됩니다.

RFC 8707 복습 — 리소스 지시자(Resource Indicators)

Lesson 16에서 그 형태를 확립했습니다. 프로덕션에서의 규칙은 다음과 같습니다. 모든 토큰 요청은 resource=<canonical-mcp-url>을 포함해야 하고, MCP 서버는 모든 호출 시 token.aud가 자신의 리소스 URL과 일치하는지 검증해야 합니다. 예를 들어 MCP 서버가 https://notes.example.com/mcp에서 접근 가능하다면, 표준(canonical) URL은 https://notes.example.com입니다. 하나의 서버가 하나의 청중(audience) 아래에서 여러 경로(path)를 호스팅할 수 있도록, 경로 구성요소(path component)는 의도적으로 제외합니다.

RFC 7636 복습 — PKCE

PKCE는 OAuth 2.1에서 필수 요소입니다. 이 lesson에서 다루는 인가 코드 흐름(authorization-code flow)은 언제나 code_challengecode_verifier를 함께 운반합니다. 서버는 검증자(verifier)가 없거나, 저장된 챌린지(challenge)와 일치하도록 해시되지 않는 검증자가 들어온 토큰 요청은 모두 거부합니다.

MCP 스펙(2025-11-25)의 인증/인가 프로파일

MCP 스펙(2025-11-25)은 MCP 서버의 인가 계층(authorization layer)이 수행해야 할 일을 매우 구체적으로 규정합니다.

  • /.well-known/oauth-protected-resource를 게시합니다(RFC 9728).
  • 토큰은 오직 Authorization: Bearer ... 헤더를 통해서만 받아들입니다.
  • 요청마다 aud, iss, exp, 그리고 필수 권한 범위(scope)를 검증합니다.
  • 모든 401과 403 응답에 대해 Bearer error=...를 담은 WWW-Authenticate 헤더를 함께 내려보내며, 적절한 경우 scope=resource= 매개변수도 포함시킵니다.
  • aud가 표준 리소스(canonical resource)와 일치하지 않는 토큰은 거부합니다.
  • iss가 보호된 리소스 메타데이터(protected-resource metadata)의 authorization_servers 목록에 없는 토큰은 거부합니다.

OAuth 2.1 초안(draft)이 기반(substrate)이라면, RFC 8414/7591/8707/9728과 RFC 7636은 그 위에 드러나는 표면(surface)이고, MCP 스펙은 이를 묶어 정의한 프로파일(profile)에 해당합니다.

IdP 역량 매트릭스(IdP capability matrix)

모든 IdP가 MCP 프로파일 전체를 지원하지는 않습니다. 아래의 매트릭스는 2025-11-25 스펙 기준으로 실제 역량(capability)을 사실 그대로 정리한 것입니다. 이는 추천 목록이 아니라 배포 차단 기준(deployment gate)으로 사용해야 합니다.

IdP 범주RFC 8414 메타데이터RFC 7591 DCRRFC 8707 리소스RFC 7636 S256 PKCE비고
자체 호스팅(Keycloak)yesyesyes (24.x 이후)yes이 lesson에서 MCP 프로파일의 기준이 되는 참조 IdP입니다. 모든 RFC를 종단 간(end-to-end)으로 지원합니다.
엔터프라이즈 SSO(Microsoft Entra ID)yesyes (상위 요금제 한정)yesyesDCR 사용 가능 여부는 테넌트(tenant) 등급에 따라 달라집니다. 배포 전 대상 테넌트에서 반드시 검증해야 합니다.
엔터프라이즈 SSO(Okta)yesyes (Okta CIC / Auth0)yesyesDCR은 Auth0(현재 Okta CIC)에서 사용 가능합니다. 기존(classic) Okta 조직은 관리자가 사전 등록을 해 두어야 합니다.
소셜 로그인 IdP(일반)variesrarelyrarelyyes대부분의 소셜 IdP는 클라이언트를 정적 파트너(static partner)로 취급합니다. DCR에 의존하지 말고 신원 제공원(identity source)으로만 사용하며, 그 위에 MCP를 인식하는 별도의 인가 서버를 한 층 더 두는 방식이 안전합니다.
자체 제작(Custom / homegrown)dependsdependsdependsdepends직접 만든다면 프로파일 전체를 구현해야 합니다. 위 네 가지 RFC 중 어느 하나라도 건너뛰면 MCP 인증/인가 계약(contract)이 깨집니다.

배포 매니페스트에 적용되는 거부 규칙은 다음과 같습니다. 선택한 IdP가 registration_endpoint를 반환하지 않거나 code_challenge_methods_supportedS256을 포함하지 않는다면, MCP 서버는 시작 자체를 거부합니다. 기능 일부만 제한된 형태로 동작하는 축소 모드(degraded mode)는 존재하지 않습니다.

iii를 사용한 JWKS 회전 패턴

프로덕션에서 가장 흔한 실패 모드는 오래된 JWKS 캐시입니다. 이 문제는 cron 트리거와 state::* 캐시 조합으로 해결합니다.

iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *", "name": "auth::jwks-refresh"},
    "auth::rotate-jwks",
)

6시간 주기로 cron 트리거가 auth::rotate-jwks를 호출합니다. 이 함수는 <issuer>/.well-known/jwks.json을 가져와 state::set("auth/jwks/<issuer>", {keys, fetched_at})에 결과를 기록합니다. 검증기(validator)는 state::get에서 이 값을 읽습니다. 만약 토큰의 kid가 캐시에 없으면, 대체 경로(fallback)로 동기(synchronous) auth::rotate-jwks 호출을 다시 한 번 트리거합니다. 이렇게 하면 두 가지 경우를 동시에 다룰 수 있습니다. 하나는 예약된 회전(cron 기반 scheduled rotation)이고, 다른 하나는 키가 겹치는 구간(key-overlap window)에서의 동기 대체 처리입니다.

상태(state)의 형태는 다음과 같습니다.

{
  "auth/jwks/https://auth.example.com": {
    "keys": [
      {"kid": "k_2026_03", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"},
      {"kid": "k_2026_04", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256", "use": "sig"}
    ],
    "fetched_at": 1772668800
  }
}

두 개의 키가 동시에 존재하는 상태가 정상적인 정상 상태(steady state)입니다. 인가 서버는 새 키(k_2026_04)를 이전 키(k_2026_03)를 폐기하기 전에 미리 도입하기 때문에, 이전 키로 발급된 토큰도 만료 시점까지는 그대로 유효합니다. 캐시는 두 키의 합집합(union)을 보관하고, 검증기는 그중에서 kid로 적절한 키를 골라 사용합니다.

iii 프리미티브 결선, 이 lesson의 실제 핵심

총 다섯 개의 프리미티브가 인증/인가 표면 전체를 구성합니다.

# 1. RFC 8414 metadata document
iii.registerTrigger(
    "http",
    {"path": "/.well-known/oauth-authorization-server", "method": "GET"},
    "auth::serve-asm",
)

# 2. RFC 7591 dynamic client registration
iii.registerTrigger(
    "http",
    {"path": "/register", "method": "POST"},
    "auth::register-client",
)

# 3. JWT validation as a callable function (the resource server triggers it)
iii.registerFunction("auth::validate-jwt", validate_jwt_handler)

# 4. Step-up issuance for incremental scope (SEP-835 from L16)
iii.registerFunction("auth::issue-step-up", issue_step_up_handler)

# 5. Cron-driven JWKS rotation
iii.registerTrigger(
    "cron",
    {"schedule": "0 */6 * * *"},
    "auth::rotate-jwks",
)
iii.registerFunction("auth::rotate-jwks", rotate_jwks_handler)

MCP 서버 자체는 검증을 직접 호출하지 않습니다. 대신 다음과 같은 방식으로 위임합니다.

result = iii.trigger("auth::validate-jwt", {"token": bearer_token, "resource": self.resource})
if not result["valid"]:
    return {"status": 401, "WWW-Authenticate": result["www_authenticate"]}

이러한 간접화(indirection)가 곧 iii가 추구하는 설계 선택입니다. 내일 검증기를 두 IdP에 병렬로 질의하는 부채꼴 분기(fanout) 구조로 바꾸거나, 스팬 방출기(span emitter)를 추가하거나, 성공적으로 검증된 결과만 따로 캐시한다 하더라도 MCP 서버 자체는 전혀 바뀌지 않아도 됩니다.

청중 결속(audience binding)으로 보는 혼동된 대리인(confused-deputy) 시나리오

서버 A(notes.example.com)와 서버 B(tasks.example.com)가 같은 인가 서버에 등록되어 있다고 가정합니다. 서버 A가 침해(compromise)되었고, 공격자는 사용자의 notes 토큰을 가로채 서버 B에 그대로 재생(replay)합니다.

서버 B의 검증기는 다음과 같이 동작합니다.

  1. JWT를 디코딩하고, kid를 기준으로 JWKS를 조회한 뒤 서명을 검증합니다.
  2. iss를 자신의 보호된 리소스 메타데이터의 authorization_servers 목록과 비교합니다. 같은 IdP이므로 통과합니다.
  3. aud == "https://tasks.example.com"인지 확인합니다. 토큰의 audhttps://notes.example.com이므로 실패합니다.
  4. WWW-Authenticate: Bearer error="invalid_token", error_description="audience mismatch" 헤더와 함께 401을 반환합니다.

청중 클레임(aud claim)은 프로토콜 계층에서 이 공격을 막아 줄 수 있는 유일한 방어 수단입니다. 성능을 핑계로 이를 건너뛰는 것은 프로덕션에서 가장 자주 발생하는 실수입니다. 검증기는 세션 시작 시점에만 실행되어서는 안 되고, 반드시 모든 요청마다 실행되어야 합니다.

실패 모드(failure mode)

  • 오래된 JWKS. 키 회전 이후 검증기가 정상적인 토큰까지 거부하게 됩니다. 해결책은 위에서 설명한 cron + 대체 경로(fallback) 패턴입니다. 새로고침 작업(refresh job) 없이 JWKS만 단독으로 캐시해서는 절대 안 됩니다.
  • aud 클레임 누락. 일부 IdP는 토큰 요청에 resource가 포함되어 있지 않으면 기본적으로 aud를 생략합니다. 검증기는 aud가 누락된 상황을 와일드카드(wildcard)로 해석해서는 안 되며, 반드시 거부해야 합니다.
  • 권한 상승 경합(scope upgrade race). 같은 사용자에 대해 동시에 진행된 두 개의 단계적 권한 상승(step-up) 흐름이 모두 성공해, 서로 다른 권한 범위(scope)를 가진 액세스 토큰(access token)이 두 개 발급되는 일이 일어날 수 있습니다. 검증기는 "사용자의 현재 scope"를 별도로 조회해서는 안 되고, 요청과 함께 제시된 토큰 자체만 사용해야 합니다. 그렇지 않으면 TOCTOU(check-to-use 사이의 시점 차이) 창이 생깁니다.
  • 등록 토큰 탈취. 유출된 registration_access_token은 공격자가 리다이렉트 URI를 임의로 다시 쓸 수 있게 해 줍니다. 따라서 이 토큰은 저장 시 해시해 두고, 업데이트할 때마다 클라이언트가 원문(cleartext)을 직접 제시하게 만들고, 의심 정황이 있으면 곧바로 회전시켜야 합니다.
  • iss 미고정. 어떤 iss든 받아들이는 검증기는 공격자가 자신만의 인가 서버를 세우고, 표적 청중(audience)에 맞는 클라이언트를 등록한 뒤 토큰을 직접 발급할 수 있게 만듭니다. 보호된 리소스 메타데이터의 authorization_servers 목록이 곧 허용 목록(allow-list)이므로, 이를 반드시 강제해야 합니다.

사용해 보기

code/main.py는 표준 라이브러리 Python과 작은 iii_mock 레지스트리(registry)만으로 프로덕션 흐름 전체를 따라갑니다. 이 레지스트리는 iii.registerFunction, iii.registerTrigger, iii.trigger, state::set/get을 흉내 냅니다. 흐름은 다음과 같습니다.

  1. 인가 서버가 /.well-known/oauth-authorization-server에 RFC 8414 메타데이터를 게시합니다.
  2. MCP 클라이언트가 메타데이터 엔드포인트를 호출하고, 그 안에서 등록 엔드포인트(registration endpoint)를 발견합니다.
  3. MCP 클라이언트가 /register(RFC 7591)에 POST하고 client_id를 받습니다.
  4. MCP 클라이언트가 resource 지시자(RFC 8707)를 포함한, PKCE로 보호되는 인가 코드 흐름(authorization code flow, RFC 7636)을 실행합니다.
  5. MCP 클라이언트가 Authorization: Bearer ... 헤더로 MCP 서버의 도구(tool)를 호출합니다.
  6. MCP 서버가 state::get에서 JWKS를 읽어 오는 auth::validate-jwt를 트리거합니다.
  7. cron 트리거가 auth::rotate-jwks를 실행해 상태(state) 안의 JWKS를 교체합니다.
  8. 다음 호출은 서버 재시작 없이도 새 키를 기준으로 검증됩니다.
  9. 다른 MCP 리소스를 향한 혼동된 대리인(confused-deputy) 시도는 청중 불일치(audience mismatch)와 함께 401을 받습니다.

여기서 사용하는 모의 JWT(mock JWT)는 lesson을 표준 라이브러리만으로 굴리기 위해, 공유 비밀(shared secret)을 사용하는 HS256 알고리즘을 씁니다. 프로덕션에서는 위에서 설명한 JWKS 패턴과 함께 RS256 또는 EdDSA를 사용합니다. 그 외 검증 로직은 모두 동일합니다.

산출물 만들기

이 lesson에서는 outputs/skill-mcp-auth-iii.md라는 산출물을 만듭니다. MCP 서버 설정(config)과 IdP의 역량(capability) 집합이 주어졌을 때, 이 스킬(skill)은 등록해야 할 iii 프리미티브, JWKS 회전 일정, 권한 범위 매핑(scope mapping), 그리고 IdP가 RFC 프로파일 전체를 지원하지 않을 때 적용해야 할 거부 규칙을 함께 내보냅니다.

연습문제

  1. code/main.py를 실행합니다. 9단계 흐름을 추적합니다. auth::rotate-jwks가 덮어쓰기 직전 state::get이 오래된 데이터를 반환하는 위치를 찾아내고, 이어지는 요청이 새 키 기준으로 검증되는 흐름을 확인합니다.

  2. 보호된 리소스 메타데이터의 authorization_servers 목록에 새로운 IdP를 추가합니다. 그 새 IdP가 서명한 토큰을 발급해 검증기가 정상적으로 수락하는지 확인합니다. 이어서 목록에 없는 IdP가 서명한 토큰을 발급해, WWW-Authenticate: Bearer error="invalid_token", error_description="iss not allowed"로 거부되는지도 확인합니다.

  3. auth::rate-limit을 iii 함수로 구현하고, 등록 처리기(registrar)가 실행되기 전에 등록 HTTP 트리거 안에서 이를 호출하도록 만듭니다. state::set("auth/ratelimit/<ip>", ...)에 보관되는, 원본 IP(source IP)별 토큰 버킷(token bucket)을 사용합니다.

  4. RFC 7591을 읽고, 이 lesson의 /register 처리기가 아직 검증하지 않는 필드 두 가지를 찾아냅니다. 그리고 해당 검증 로직을 직접 추가합니다. 힌트: software_statementredirect_uris의 URI 스킴(URI scheme)입니다.

  5. MCP 스펙(2025-11-25)의 authorization 섹션을 읽습니다. 이 lesson의 검증기가 현재 내보내고 있지 않은 WWW-Authenticate 헤더 관련 규범적 요구사항(normative requirement) 한 가지를 찾아내, 이를 구현에 추가합니다.

핵심 용어

용어흔한 설명실제 의미
ASM(Authorization Server Metadata)"OAuth 메타데이터 문서"RFC 8414 /.well-known/oauth-authorization-server JSON 문서
DCR(Dynamic Client Registration)"셀프서비스 클라이언트 등록"RFC 7591의 POST /register 흐름
JWKS(JSON Web Key Set)"JWT 검증용 공개 키"jwks_uri에서 가져오고 kid로 색인되는 JSON Web Key Set
리소스 지시자(Resource indicator)"청중 매개변수(Audience parameter)"토큰을 단일 서버에 고정하는 RFC 8707의 resource 매개변수
aud 클레임(claim)"청중(Audience)"검증기가 표준 리소스 URL(canonical resource URL)과 비교하는 JWT 클레임
혼동된 대리인(Confused deputy)"토큰 재생(Token replay)"서버 A용으로 발급된 토큰이 서버 B에 그대로 제시되는 공격
iss 허용 목록(allow-list)"신뢰하는 인가 서버"보호된 리소스 메타데이터의 authorization_servers에 등재된 집합
키 회전(Key rotation)"롤링 JWKS(Rolling JWKS)"겹침 구간(overlap window)을 두고 서명 키를 주기적으로 교체하는 것
공개 클라이언트(Public client)"네이티브 또는 브라우저 클라이언트"client_secret이 없는 OAuth 클라이언트로, PKCE가 그 약점을 보완함
WWW-Authenticate"401/403 응답 헤더"클라이언트의 복구 동작을 유도하는 Bearer error=... 지시자를 담는 헤더

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

mcp-auth-iii-wiring

Wire production MCP authorization (RFC 8414, 7591, 8707, 7636 PKCE, 9728) onto iii primitives — registerTrigger for HTTP/cron, registerFunction for validation, state::* for JWKS cache.

Skill

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.프로덕션에서 프로덕션 MCP 인증의 가장 중요한 설계 원칙은?

2.프로덕션 MCP 인증가 올바른 선택이 아닌 경우는?

3.프로덕션 MCP 인증는 AI 생태계에 어떻게 들어맞나요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다