MCP OBO 인증

MCP OBO 인증 (OBO Auth)

OAuth 2.0 On-Behalf-Of(OBO) 인증을 사용하면 LiteLLM이 사용자의 들어오는 베어러 토큰을 특정 MCP 서버에 유효한 스코프 토큰으로 교환해요.

OBO를 사용할 때:

  • MCP 서버가 특정 MCP 서버용으로 발급된 토큰을 받아야 할 때.
  • ID 공급자가 RFC 8693 OAuth 2.0 Token Exchange를 지원하거나, LiteLLM이 네이티브로 말하는 Microsoft Entra ID일 때(아래 Microsoft Entra ID 참고).
  • LiteLLM이 사용자 원시 토큰을 MCP 서버로 직접 전달하지 않도록 하고 싶을 때.

출처: 문서

본문

동작 방식 (How It Works)

요약하자면:

  1. 클라이언트가 베어러 토큰과 함께 LiteLLM에 요청을 보냅니다.
  2. LiteLLM은 그 베어러 토큰을 RFC 8693 subject_token으로 사용해요.
  3. LiteLLM은 ID 공급자의 토큰 교환 엔드포인트에서 그것을 교환해요.
  4. LiteLLM은 교환된 스코프 토큰만 MCP 서버로 전달해요.
  5. LiteLLM은 교환된 토큰이 만료될 때까지 캐시하므로 반복 호출이 ID 공급자 왕복을 피해요.

MCP 서버를 OBO용으로 구성 (Configure an MCP Server for OBO)

MCP 서버에 auth_type: oauth2_token_exchange를 설정하세요.

config.yaml:

mcp_servers:
  internal_tools:
    url: "https://mcp.example.com/mcp"
    transport: "http"
    auth_type: oauth2_token_exchange

    # OAuth 2.0 Token Exchange endpoint on your identity provider
    token_exchange_endpoint: "https://idp.example.com/oauth2/token"

    # Token exchange client registered with your identity provider
    client_id: "<idp-client-id>"
    client_secret: "<idp-client-secret>"

    # Optional but recommended: restrict the exchanged token to this MCP server
    audience: "api://internal-tools-mcp"
    scopes:
      - "mcp.tools.read"
      - "mcp.tools.execute"

    # Optional. Defaults to access_token.
    subject_token_type: "urn:ietf:params:oauth:token-type:access_token"

구성 필드 (Config Fields)

필드 필수 설명
auth_type oauth2_token_exchange여야 함.
token_exchange_endpoint RFC 8693 토큰 교환 요청을 받는 ID 공급자 엔드포인트.
client_id 토큰 교환 엔드포인트를 호출할 때 LiteLLM이 사용하는 OAuth 클라이언트 식별자.
client_secret 토큰 교환 엔드포인트를 호출할 때 LiteLLM이 사용하는 OAuth 클라이언트 시크릿.
audience 권장 MCP 서버의 리소스 식별자. LiteLLM이 이를 토큰 교환 audience로 전송.
scopes 선택 교환된 토큰에 대해 LiteLLM이 요청하는 스코프. LiteLLM이 목록을 OAuth scope 파라미터로 연결.
subject_token_type 선택 RFC 8693 subject 토큰 타입. 기본값 urn:ietf:params:oauth:token-type:access_token.
upstream_token_header 선택 교환된 토큰을 담는 업스트림 헤더. 기본값 Authorization. 다른 헤더로 토큰 보내기 참고.
token_exchange_profile 선택 교환의 와이어 방언. rfc8693(기본)은 표준 토큰 교환 그랜트, entra_obo는 Microsoft Entra ID의 On-Behalf-Of 흐름. Microsoft Entra ID 참고.

교환된 토큰을 다른 헤더로 보내기 (Sending the exchanged token on a different header)

기본적으로 교환된 토큰은 Authorization: Bearer ***로 나가요. MCP 서버가 자체 자격 증명을 사설 헤더에서 읽는 API 게이트웨이 뒤에 있고, 게이트웨이 뒤의 서버가 여전히 Authorization`에 자체 베어러를 기대한다면 두 자격 증명 모두 같은 요청에 실어야 해요.

upstream_token_header를 설정해 교환된 토큰이 사용할 헤더를 지정하세요. 그러면 static_headers 아래의 모든 것은 그대로 남아 게이트웨이 뒤의 서버에 공유 자격 증명이 계속 도달해요.

config.yaml:

mcp_servers:
  my_mcp_server:
    url: "https://gateway.example.com/mcp"
    auth_type: oauth2_token_exchange
    token_exchange_endpoint: "https://idp.example.com/token"
    client_id: os.environ/MCP_CLIENT_ID
    client_secret: os.environ/MCP_CLIENT_SECRET
    audience: "api://esb"
    upstream_token_header: "esb-oauth"
    static_headers:
      Authorization: "Bearer os.environ/UPSTREAM_MCP_TOKEN"

그러면 각 업스트림 요청이 둘 다를 전달하고, 교환된 토큰은 호출 사용자 스코프로 제한돼요:

esb-oauth: Bearer <token exchanged for this user>
Authorization: Bearer *** shared token you configured>

교환된 토큰은 여전히 사용자별로 캐시되므로 단기 토큰이 모든 요청에서 교환을 의미하지는 않아요. upstream_token_header를 미설정으로 두면 기본값이 유지돼요.

업스트림에서 재인출(redirect)이 원본(origin)을 건너면 HTTP 클라이언트가 Authorization을 버리듯 사용자 지정 헤더도 전달되지 않고 버려져요. 원본 간 재인출을 정당하게 하는 업스트림은 두 번째 홉에서 자격 증명을 보지 못합니다.

토큰 교환 요청 (Token Exchange Request)

캐시되지 않은 subject 토큰과 MCP 서버 쌍마다 LiteLLM은 다음처럼 폼 인코딩 요청을 token_exchange_endpoint에 보내요:

POST /oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<caller-bearer-token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&client_id=<idp-client-id>
&client_secret=<idp-client-secret>
&audience=api://internal-tools-mcp
&scope=mcp.tools.read mcp.tools.execute

ID 공급자는 액세스 토큰을 반환해야 해요:

{
  "access_token": "scoped-token-for-mcp-server",
  "token_type": "Bearer",
  "expires_in": 3600
}

그런 다음 LiteLLM이 MCP 서버를 호출합니다:

Authorization: Bearer scoped...rver

Microsoft Entra ID (Azure AD)

Microsoft Entra ID는 위의 RFC 8693 토큰 교환 그랜트를 구현하지 않아요. On-Behalf-Of 흐름은 대신 RFC 7523 jwt-bearer 그랜트를 사용해요. 호출자 토큰이 subject_token이 아니라 assertion으로 실리고, audience 파라미터가 없으며, Microsoft 전용 requested_token_use=on_behalf_of 확장이 그랜트를 단순 jwt-bearer 교환이 아닌 위임으로 만들어요. LiteLLM은 Entra를 일급 프로필로 취급하므로 Entra를 가리키는 것은 통합을 바꾸는 것이 아니라 구성 변경이에요. token_exchange_profile: entra_obo를 설정하면 LiteLLM이 RFC 8693 형식 대신 jwt-bearer 형식을 구축해요.

config.yaml:

mcp_servers:
  internal_tools:
    url: "https://mcp.example.com/mcp"
    transport: "http"
    auth_type: oauth2_token_exchange
    token_exchange_profile: entra_obo

    # Your Entra tenant's v2.0 token endpoint
    token_exchange_endpoint: "https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token"

    # App registration LiteLLM uses to call the token endpoint. The caller's
    # token must have been issued with this client_id as its `aud`.
    client_id: "<entra-app-client-id>"
    client_secret: "<entra-app-client-secret>"

    # Entra has no audience parameter, so the target resource goes in scope
    # as <app-id-uri>/.default
    scopes:
      - "api://internal-tools-mcp/.default"

audiencesubject_token_typeentra_obo에서는 사용되지 않아요. Entra에는 audience 파라미터가 없고(대상 리소스는 scope에 들어감), jwt-bearer 그랜트는 subject 토큰 타입을 검사하지 않기 때문이에요.

캐시되지 않은 호출자 토큰과 MCP 서버 쌍마다 LiteLLM이 다음을 보내요:

POST /<tenant-id>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<caller-bearer-token>
&scope=api://internal-tools-mcp/.default
&requested_token_use=on_behalf_of
&client_id=<entra-app-client-id>
&client_secret=<entra-app-client-secret>

Entra는 위에 보인 것과 같은 액세스 토큰 응답 형태를 반환하고, LiteLLM은 프로필과 무관하게 같은 방식으로 교환된 토큰을 캐시·전달해요.

note — 호출자 토큰은 다른 Entra 앱이 아니라 LiteLLM의 앱 등록용으로 발급되어야 해요. 하네스가 다른 앱 등록에 대해 인증한다면, 먼저 이 앱의 스코프에 대한 토큰을 요청한 후 그 토큰을 LiteLLM에 보내게 하세요.

OBO MCP 서버 호출 (Calling an OBO MCP Server)

들어오는 요청은 LiteLLM이 교환할 subject_token을 가지도록 사용자 베어러 토큰을 포함해야 해요.

직접 MCP 호출의 경우 LiteLLM 키를 x-litellm-api-key에 두고 Authorization은 사용자 토큰에 비워 두세요:

직접 MCP 호출:

curl -X POST "https://litellm.example.com/internal_tools/mcp" \
  -H "Content-Type: application/json" \
  -H "x-litellm-api-key: *** <litellm-api-key>" \
  -H "Authorization: Bearer ***" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Responses API의 경우 LiteLLM 키를 사용자 토큰과 분리해 MCP 도구 헤더로 전달하세요:

OBO 포함 Responses API:

curl -X POST "https://litellm.example.com/v1/responses" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ***" \
  -d '{
    "model": "gpt-5.6-terra",
    "input": "List the available internal tools",
    "tools": [
      {
        "type": "mcp",
        "server_label": "internal_tools",
        "server_url": "https://litellm.example.com/internal_tools/mcp",
        "require_approval": "never",
        "headers": {
          "x-litellm-api-key": "Bearer <litellm-api-key>",
          "Authorization": "Bearer <user-token>"
        }
      }
    ]
  }'

tip — MCP 클라이언트가 Authorization 헤더 하나만 보낼 수 있다면 LiteLLM 키에 x-litellm-api-key를 사용하고 Authorization은 사용자 토큰에 예약하세요. LiteLLM은 OBO subject_token으로 사용자 토큰이 필요해요.

캐싱 동작 (Caching Behavior)

LiteLLM은 다음으로 교환된 토큰을 캐시해요:

  • subject 토큰
  • MCP 서버 ID

즉 두 명의 다른 사용자는 별도 교환 토큰을 받고, 같은 사용자의 같은 MCP 서버에 대한 반복 호출은 만료될 때까지 캐시된 토큰을 재사용해요.

캐시 TTL은 expires_in에서 LiteLLM의 OAuth 만료 버퍼를 뺀 값을 기반으로 해요. expires_in이 없거나 유효하지 않으면 LiteLLM은 기본 OAuth 토큰 캐시 TTL을 사용해요.

폴백 동작 (Fallback Behavior)

OBO 서버에 들어오는 subject 토큰이 없으면:

  • client_id, client_secret, token_url이 구성되어 있으면 LiteLLM이 OAuth client_credentials로 폴백할 수 있어요.
  • 그렇지 않으면 LiteLLM이 경고를 기록하고 토큰 교환 없이 진행해요.

엄격한 OBO 배포의 경우 모든 요청에 사용자 베어러 토큰이 포함되도록 클라이언트를 구성하세요.

문제 해결 (Troubleshooting)

증상 확인할 것
MCP 서버가 LiteLLM 키를 받음 LiteLLM 키를 x-litellm-api-key로 옮기고 Authorization은 사용자 토큰에 사용.
토큰 교환 엔드포인트가 400 반환 audience, scopes, client_id, subject_token_type이 ID 공급자 구성과 일치하는지 확인.
MCP 서버가 Authorization 헤더를 받지 못함 MCP 서버에 auth_type: oauth2_token_exchange가 있고 들어오는 요청에 사용자 베어러 토큰이 포함되었는지 확인.
ID 공급자를 매 요청마다 호출 ID 공급자가 expires_in을 반환하는지, 같은 사용자 토큰과 MCP 서버가 재사용되는지 확인.

더 알아보기 (Learn more)