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)
요약하자면:
- 클라이언트가 베어러 토큰과 함께 LiteLLM에 요청을 보냅니다.
- LiteLLM은 그 베어러 토큰을 RFC 8693
subject_token으로 사용해요. - LiteLLM은 ID 공급자의 토큰 교환 엔드포인트에서 그것을 교환해요.
- LiteLLM은 교환된 스코프 토큰만 MCP 서버로 전달해요.
- 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"
audience와 subject_token_type은 entra_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은 OBOsubject_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이 OAuthclient_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 서버가 재사용되는지 확인. |