ID 어설션
기계 번역
이 페이지는 영어 문서를 자동으로 번역한 것이며, 영어 페이지가 기준이 되는 정식 버전입니다. 어색하거나 잘못된 부분이 있다면 번역 페이지에서 제보하는 방법을 확인하세요.
일반적인 OAuth 공급자(OAuth 클라이언트)는 먼저 MCP 서버에 어느 인가 서버를 신뢰하는지 묻는 것으로 시작합니다. 그 답이 가리키는 곳이면 어디든 따라가고, 그런 다음 사람이 로그인하거나 사전 공유된 시크릿이 사람을 대신합니다.
기업은 이 둘 중 어느 것도 서버마다 따로 결정되기를 원하지 않습니다. 기업은 이미 ID 공급자(Okta, Microsoft Entra ID, 또는 자체 운영하는 것)를 운영하고 있고, 사용자는 오늘 아침에 이미 거기에 로그인했으며, 보안 팀이 누가 무엇에 접근할 수 있는지를 결정하고 싶어 하는 유일한 곳이 바로 그곳입니다. Enterprise-Managed Authorization 확장인 SEP-990은 그 결정을 그곳으로 옮깁니다. IdP는 수명이 짧은 JWT, 즉 Identity Assertion JWT Authorization Grant, 줄여서 ID-JAG에 서명합니다. 이 사용자가 이 클라이언트를 통해 이 MCP 서버에 접근해도 된다는 진술입니다. 클라이언트는 이를 일반적인 액세스 토큰으로 교환합니다. 브라우저도, 동의 화면도, 동적 등록도 없습니다.
이 페이지는 그 교환의 양쪽 끝을 모두 다룹니다. MCP 서버 자체는 전혀 바뀌지 않습니다. 여전히 인가에서 본 리소스 서버이며, 들어오는 토큰이 무엇이든 검사할 뿐입니다.
두 번의 토큰 요청
여기에는 서로 다른 두 권한 주체가 등장하며, 이 둘을 구분해서 부르는 것이 이 페이지를 이해하는 일의 대부분입니다. 엔터프라이즈 IdP는 조직의 ID 공급자입니다. 직원이 누구인지 알고, 정책이 있는 곳이며, ID-JAG를 발급합니다. SDK는 이 IdP와 절대 통신하지 않습니다. MCP 인가 서버는 인가에서와 같은 당사자입니다. MCP 서버의 메타데이터에 명시된 발급자이자, 그 MCP 서버가 받아들이는 토큰을 발급하는 주체입니다. 일반적인 OAuth 흐름에서는 이 두 역할이 보통 하나의 시스템입니다. 여기서는 둘로 나뉘며, 이 그랜트 전체는 결국 후자가 전자를 신뢰하기로 동의하는 것입니다.
클라이언트는 각각에 토큰 요청을 한 번씩 보냅니다.
- 엔터프라이즈 IdP에 보내는 요청. 클라이언트는 사용자의 로그인(OpenID Connect ID 토큰)을 ID-JAG로 교환합니다. 이것은 RFC 8693 토큰 교환이고, 전적으로 IdP의 API이며, SDK는 이 요청을 보내지 않습니다. 하나의 async 콜백 안에서 직접 보냅니다. 정책 결정이 일어나는 곳도 여기입니다. IdP가 거부하면 ID-JAG는 발급되지 않고, 제시할 것도 없습니다.
- MCP 인가 서버에 보내는 요청. 클라이언트는 RFC 7523
jwt-bearer그랜트(grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, ID-JAG를assertion으로)로 ID-JAG를 제시하고 액세스 토큰을 받습니다. 이것이 SDK가 보내는 요청이며, 이 요청을 받아들이는 것이 이 페이지가 인가 서버에 추가하는 단 하나의 기능입니다.
아래의 모든 내용은 두 번째 요청, 즉 이를 보내는 클라이언트와 이에 응답하는 인가 서버에 관한 것입니다.
클라이언트
IdentityAssertionOAuthProvider는 mcp.client.auth.extensions.identity_assertion에 있습니다. OAuth 클라이언트의 모든 공급자와 마찬가지로 httpx2.Auth입니다. 하나를 생성해 auth=에 넣고, httpx2.AsyncClient를 트랜스포트에 넘기면 됩니다.
import time
import uuid
import httpx2
import jwt
from mcp import Client
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
IDP_SIGNING_KEY = "the-enterprise-idp-signing-key-for-this-demo"
class InMemoryTokenStorage:
def __init__(self) -> None:
self.tokens: OAuthToken | None = None
self.client_info: OAuthClientInformationFull | None = None
async def get_tokens(self) -> OAuthToken | None:
return self.tokens
async def set_tokens(self, tokens: OAuthToken) -> None:
self.tokens = tokens
async def get_client_info(self) -> OAuthClientInformationFull | None:
return self.client_info
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
self.client_info = client_info
def idp_issue_id_jag(subject: str, audience: str, resource: str) -> str:
now = int(time.time())
claims = {
"iss": "https://idp.example.com",
"sub": subject,
"aud": audience,
"client_id": "finance-agent",
"resource": resource,
"scope": "notes:read",
"jti": str(uuid.uuid4()),
"iat": now,
"exp": now + 300,
}
return jwt.encode(claims, IDP_SIGNING_KEY, algorithm="HS256", headers={"typ": "oauth-id-jag+jwt"})
async def fetch_id_jag(audience: str, resource: str) -> str:
return idp_issue_id_jag("alice@example.com", audience, resource)
oauth = IdentityAssertionOAuthProvider(
server_url="http://localhost:8001/mcp",
storage=InMemoryTokenStorage(),
client_id="finance-agent",
client_secret="finance-agent-secret",
issuer="https://auth.example.com/",
assertion_provider=fetch_id_jag,
scope="notes:read",
)
async def main() -> None:
async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client:
transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client)
async with Client(transport) as client:
result = await client.list_tools()
print([tool.name for tool in result.tools])
아래에서부터 읽어 보세요.
main()은 표준 OAuth 클라이언트의main()(OAuth 클라이언트)이며, 한 줄도 바뀌지 않았습니다. 그것이 핵심입니다. 공급자가 일단 존재하면, 그 뒤의 어떤 코드도 어떤 그랜트가 토큰을 만들어 냈는지 알지 못합니다.- 공급자는 다른 공급자가 스스로 알아낼 수 없는 것을 받습니다. 누군가가 인가 서버에 사전 등록해 둔
client_id와client_secret, 그 인가 서버의issuer, 그리고 요청할 때마다 새 ID-JAG를 반환하는 async 콜백인assertion_provider입니다. storage는 같은TokenStorage프로토콜입니다. 호출되는 것은 토큰 관련 메서드 두 개뿐입니다. 여기에는 동적 등록이 없으므로 기억해 둘client_info도 없습니다.
어설션 공급자
fetch_id_jag(audience, resource)가 직접 작성하는 유일한 코드입니다. 토큰 교환마다 한 번씩 await되고, 생성 시점에는 절대 호출되지 않으며, 인가 서버의 메타데이터를 가져와 검증한 뒤에만 호출되므로 잘못 설정된 발급자로 어설션이 새어 나가는 일은 없습니다. 두 인수는 ID-JAG를 발급할 때 담아야 하는 클레임 중 두 가지입니다. audience는 인가 서버의 발급자(ID-JAG의 aud)이고 resource는 MCP 서버의 정식 식별자(ID-JAG의 resource)입니다. 세 번째는 이미 가지고 있는 값입니다. ID-JAG의 client_id 클레임은 공급자에 넘긴 client_id를 가리켜야 하며, 그렇지 않으면 인가 서버가 교환을 거부합니다.
그 위에 있는 idp_issue_id_jag는 작성할 코드가 아닙니다. ID 공급자를 대신하는 것으로, 파일이 그 자체로 완결되고 ID-JAG가 담는 모든 클레임을 읽어 볼 수 있도록 프로세스 안에서 어설션에 서명합니다. 실제 fetch_id_jag는 그 대신 앞 절의 첫 번째 토큰 요청을 보냅니다. IdP를 상대로 한 RFC 8693 토큰 교환이며, SEP-990이 프로파일로 삼는 Identity Assertion JWT Authorization Grant 초안이 이를 정의합니다. 로그인한 사용자의 ID 토큰이 subject_token으로 들어가고, requested_token_type은 ID-JAG 고유의 URN(urn:ietf:params:oauth:token-type:id-jag)이며, audience와 resource는 그대로 전달되고, 응답에 ID-JAG가 실려 옵니다. IdP 문서에서 찾아봐야 할 것이 바로 이 이름들로 이루어지는 이 교환입니다.
Tip
교환할 때마다 새 ID-JAG를 요청하며, 그것이 의도된 설계입니다. ID-JAG는 수명이 몇 분에 불과한 일회용 그랜트이고, 이 페이지의 인가 서버는 같은 ID-JAG를 두 번 받아들이지 않습니다. 캐시하지 마세요. 재사용되는 것은 ID-JAG로 얻은 액세스 토큰입니다.
설정으로 지정하는 발급자
여기서 관계가 뒤집힙니다. OAuthClientProvider는 어느 인가 서버를 쓸지 리소스 서버에 묻고 그 답이 가리키는 곳이면 어디든 따라갑니다. 이 공급자는 그렇게 하기를 거부합니다. issuer는 필수이고, RFC 8414 메타데이터는 그 발급자 자신의 well-known 경로에서 가져오며, 토큰 엔드포인트는 그 발급자의 오리진에 있어야 하고, 리소스 서버에는 아무것도 묻지 않습니다.
확장이 이를 요구하는 것은 아닙니다. 의도적으로 더 엄격하게 선택한 것입니다. 이 클라이언트는 훔칠 가치가 있는 것을 두 가지 지니고 있습니다. 사전 등록된 시크릿과 audience에 묶인 어설션입니다. 침해된 MCP 서버가 공격자의 인가 서버로 유도하도록 내버려 두는 클라이언트라면 둘 다 그곳에 POST하게 됩니다. 생성 시점에 발급자를 고정하면 그런 대화 자체가 사라집니다.
Warning
설정한 issuer는 메타데이터 문서의 issuer 필드와 RFC 8414 §3.3의 단순 문자열 비교로
대조됩니다. 한 글자씩, 끝의 슬래시까지 포함해, 정규화 없이 비교합니다. 추측하지 마세요. 인가
서버에서 /.well-known/oauth-authorization-server를 가져와 반환된 issuer 값을 그대로
복사하세요. 이 페이지의 인가 서버라면 그 값은 슬래시가 붙은 https://auth.example.com/입니다.
발급자가 pydantic URL 객체로부터 만들어졌기 때문입니다. 일치하지 않으면 자격 증명이나 어설션을
단 하나도 보내기 전에 OAuthFlowError: Authorization server metadata issuer
mismatch에서 흐름이 멈춥니다.
기밀 클라이언트
client_secret은 필수이며, 없으면 생성자가 ValueError를 발생시킵니다. SEP-990의 기반이 되는 IETF 프로파일은 이 그랜트를 기밀 클라이언트 전용으로 두고, SEP-990은 클라이언트가 인증할 것을 요구하며, 이 SDK는 공유 시크릿을 반드시 요구함으로써 둘 다 강제합니다. token_endpoint_auth_method는 시크릿이 어디에 실려 가는지를 고릅니다. client_secret_post(기본값, 폼 본문에)나 client_secret_basic(HTTP Basic 헤더) 중 하나입니다. 프로파일은 private_key_jwt도 허용하지만, 이 공급자는 지원하지 않습니다.
Tip
client_secret은 환경 변수나 시크릿 관리자에서 읽어 오세요. 소스 관리에서 읽어서는 절대 안 됩니다.
공급자가 대신 처리하는 일
첫 번째 요청은 인증 없이 나가고, 서버의 401이 흐름을 시작합니다.
- 탐색. 설정된 발급자의 RFC 8414 well-known 경로에서 인가 서버 메타데이터를 가져오고, 문서의
issuer가 일치하는지 확인하고, 토큰 엔드포인트가 발급자의 오리진에 있는지 확인합니다. - 어설션.
assertion_provider를 await합니다. - 교환. 토큰 엔드포인트에
jwt-bearer그랜트를 POST하고,OAuthToken을 저장한 뒤, 원래 요청을Authorization: Bearer ...를 붙여 다시 보냅니다.
WWW-Authenticate에 insufficient_scope가 명시된 403을 받으면 설정한 scope와 챌린지로 요구된 범위의 합집합으로 2단계와 3단계를 다시 실행합니다. (scope는 어디까지나 요청일 뿐입니다. 이 페이지의 인가 서버는 ID-JAG에 적힌 것만 부여하고 그 외에는 아무것도 부여하지 않습니다.) 이 과정 어디에도 리프레시 토큰은 없습니다. 액세스 토큰이 만료되면 다음 401에서 새 ID-JAG를 발급받아 다시 교환하며, 바로 그것이 IdP가 쥐고 있는 지렛대입니다. 실패는 OAuth 클라이언트의 나머지와 같은 두 가지 예외로 나타납니다. 탐색과 검증에는 OAuthFlowError, 토큰 엔드포인트가 거부하면 그 하위 클래스인 OAuthTokenError입니다.
인가 서버
대부분의 경우 여기서 멈추면 됩니다. MCP 인가 서버는 다른 누군가의 제품이고, ID-JAG를 받아들이는 것은 그 제품에서 켜야 할 설정이며, SEP-990에서 SDK가 맡는 절반은 위의 클라이언트입니다.
SDK가 직접 인가 서버가 될 수도 있습니다. create_auth_routes는 인가 서버의 라우트를 어떤 Starlette 앱이든 마운트할 수 있는 리스트로 반환하며, 저장소의 examples/servers/simple-auth/가 바로 이 방식으로 인가 서버를 실행합니다. SEP-990은 그 표면에 플래그 하나와 메서드 하나를 추가합니다.
import secrets
import time
import jwt
from pydantic import AnyHttpUrl
from starlette.applications import Starlette
from mcp.server.auth.provider import (
AccessToken,
AuthorizationCode,
AuthorizationParams,
AuthorizeError,
IdentityAssertionParams,
OAuthAuthorizationServerProvider,
RefreshToken,
TokenError,
)
from mcp.server.auth.routes import create_auth_routes
from mcp.shared.auth import JWT_BEARER_GRANT_TYPE, OAuthClientInformationFull, OAuthToken
ISSUER = "https://auth.example.com/"
MCP_SERVER = "http://localhost:8001/mcp"
IDP_ISSUER = "https://idp.example.com"
IDP_SIGNING_KEY = "the-enterprise-idp-signing-key-for-this-demo"
REGISTERED_CLIENTS = {
"finance-agent": OAuthClientInformationFull(
client_id="finance-agent",
client_secret="finance-agent-secret",
redirect_uris=None,
grant_types=[JWT_BEARER_GRANT_TYPE],
token_endpoint_auth_method="client_secret_post",
)
}
class EnterpriseAuthorizationServer(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
def __init__(self) -> None:
self.access_tokens: dict[str, AccessToken] = {}
self.seen_jtis: set[str] = set()
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
return REGISTERED_CLIENTS.get(client_id)
async def load_access_token(self, token: str) -> AccessToken | None:
return self.access_tokens.get(token)
async def exchange_identity_assertion(
self, client: OAuthClientInformationFull, params: IdentityAssertionParams
) -> OAuthToken:
try:
header = jwt.get_unverified_header(params.assertion)
claims = jwt.decode(
params.assertion,
IDP_SIGNING_KEY,
algorithms=["HS256"],
issuer=IDP_ISSUER,
audience=ISSUER,
options={"require": ["iss", "sub", "aud", "exp", "iat", "jti", "client_id", "resource", "scope"]},
)
except jwt.InvalidTokenError as error:
raise TokenError("invalid_grant", "the assertion did not verify") from error
if header.get("typ") != "oauth-id-jag+jwt":
raise TokenError("invalid_grant", "the assertion is not an ID-JAG")
if claims["client_id"] != client.client_id:
raise TokenError("invalid_grant", "the assertion was issued to a different client")
if claims["resource"] != MCP_SERVER:
raise TokenError("invalid_target", "the assertion is for a resource this server does not serve")
if claims["jti"] in self.seen_jtis:
raise TokenError("invalid_grant", "the assertion has already been used")
self.seen_jtis.add(claims["jti"])
scopes = claims["scope"].split()
access_token = f"mcp_{secrets.token_hex(16)}"
self.access_tokens[access_token] = AccessToken(
token=access_token,
client_id=claims["client_id"],
scopes=scopes,
expires_at=int(time.time()) + 300,
resource=claims["resource"],
subject=claims["sub"],
)
return OAuthToken(access_token=access_token, token_type="Bearer", expires_in=300, scope=" ".join(scopes))
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
raise AuthorizeError("unauthorized_client", "this authorization server only accepts ID-JAGs")
async def load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> None:
return None
async def exchange_authorization_code(
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
) -> OAuthToken:
raise TokenError("invalid_grant", "this authorization server only accepts ID-JAGs")
async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> None:
return None
async def exchange_refresh_token(
self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
) -> OAuthToken:
raise TokenError("invalid_grant", "this authorization server only accepts ID-JAGs")
provider = EnterpriseAuthorizationServer()
auth_app = Starlette(
routes=create_auth_routes(provider, issuer_url=AnyHttpUrl(ISSUER), identity_assertion_enabled=True)
)
identity_assertion_enabled=True가 모든 것의 관문입니다. 꺼져 있으면(기본값), 훅을 구현했더라도/token은 이 그랜트에unsupported_grant_type으로 응답하고 메타데이터에도 언급되지 않습니다. 켜면 메타데이터에jwt-bearer그랜트 유형이 추가되고, 확장이 지원을 알리는 데 쓰는 필드인authorization_grant_profiles_supported에urn:ietf:params:oauth:grant-profile:id-jag가 나열됩니다. (이 SDK의 클라이언트는 이 필드를 읽지 않습니다. 발급자 하나에 맞춰 프로비저닝되어 있으므로 그냥 요청할 뿐입니다.)exchange_identity_assertion이 훅입니다. 이 훅이 실행되기 전에 SDK는 이미 클라이언트를 인증하고, 공개 클라이언트를 거부하고, 등록 정보에 이 그랜트가 나열되지 않은 클라이언트를 거부한 상태입니다.IdentityAssertionParams(원시assertion, 요청된scopes와resource)를 받아 평범한OAuthToken을 반환합니다.- 동적 클라이언트 등록은 이 그랜트를 무조건 거부하므로, 여기서
get_client는 수동으로 프로비저닝한 클라이언트를 내줍니다. ID-JAG 클라이언트는 스스로 등록해서 생겨날 수 없습니다. - 클래스의 절반은 거부 코드입니다.
OAuthAuthorizationServerProvider는 인가 서버 전체이므로 인가 코드 흐름도 요구합니다. 사용자 로그인까지 처리하는 서버라면 그 부분을 실제로 구현하지만, 이 서버에는 문이 정확히 하나뿐입니다.
Warning
SDK는 어설션을 절대 디코딩하지 않습니다. 어떤 IdP를 신뢰하고 그 IdP가 어떤 키를 공개하는지는
해당 배포 환경만 알기 때문이며, 따라서 exchange_identity_assertion 안의 모든 코드가 보안을
떠받칩니다. RFC 7523 §3에 따라 IdP가 공개한 키(JWKS, 여기서 쓰는 공유 시크릿은
데모용입니다)로 서명을 검증하고, iss와 exp도 검증하세요. JWT 헤더의 typ이
oauth-id-jag+jwt일 것을 요구하세요. 다른 JWT가 그랜트로 재사용되는 것을 막는 프로파일의
안전장치입니다. aud가 자기 자신의 발급자일 것을 요구하세요. ID-JAG의 client_id 클레임이
핸들러가 인증한 클라이언트와 같을 것을, 그리고 resource 클레임이 실제로 서비스하는 리소스를
가리킬 것을 요구하세요. 어설션이 한 번만 받아들여지도록 어설션의 exp까지 jti를 추적하세요.
그리고 부여하는 범위와, 무엇보다도 발급하는 토큰의 resource는 검증된 ID-JAG에서 가져오고
절대 요청에서 가져오지 마세요. params.resource는 클라이언트가 입력한 값일 뿐입니다. 전체
처리 규칙은 Enterprise-Managed Authorization 사양에
있습니다.
잘못된 어설션은 TokenError("invalid_grant", ...)로 거부하세요. 이 흐름의 다른 오류 코드는 invalid_target입니다. 서비스하지 않는 리소스를 가리키는 ID-JAG는 이 코드로 거부되며, 이것이 이 서버가 다른 누군가의 리소스용 토큰을 발급하지 못하게 막는 장치입니다. 그리고 부여되는 범위는 ID-JAG의 scope 클레임에서 옵니다(이 클레임이 없는 어설션도 거부됩니다). 실제 구현에서는 대신 사용자의 그룹을 매핑할 수도 있습니다.
반환되는 OAuthToken에 무엇이 없는지도 눈여겨보세요. 리프레시 토큰이 없습니다. IdP는 다음 ID-JAG를 발급할지 말지를 결정함으로써 이 사용자가 얼마나 오래 접근을 유지할지 결정합니다. 여기서 리프레시 토큰을 발급하면 그 결정권을 조용히 되돌려주는 셈이 됩니다.
Info
여전히 auth_server_provider=로 인가 서버를 내장하는 서버는
AuthSettings(identity_assertion_enabled=True)를 통해 같은 코드에 도달합니다. 새 서버가 왜 그
방식으로 시작하면 안 되는지는 인가에서 설명합니다.
Check
이 페이지의 두 파일을 서로 연결하면 그랜트 전체가 POST /token 한 번입니다.
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
client_id=finance-agent
resource=http://localhost:8001/mcp
scope=notes:read
client_secret=finance-agent-secret
HTTP/1.1 200 OK
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
/authorize도, /register도, protected-resource-metadata 가져오기도 없습니다. 실제로 오가는
요청은 401을 끌어낸 요청, well-known 가져오기, 이 교환, 그리고 그 뒤로 bearer를 붙인 일반적인
MCP 트래픽뿐입니다. 그리고 검증 코드가 ID-JAG에서 읽어 낸 sub는 도구 안에서
get_access_token().subject가 보고하는 값과 정확히 같습니다.
직접 해 보기
SDK 저장소의 examples/stories/identity_assertion/은 이 페이지를 실제로 실행한 것입니다. 같은 exchange_identity_assertion 검증 코드, 그 토큰으로 보호되는 MCP 서버, 대역 IdP, 그리고 클라이언트까지, 스스로 검증하는 프로그램 하나에 모두 담겨 있습니다. uv run python -m stories.identity_assertion.client --http는 교환 전체를 실행하고 IdP가 지명한 사용자가 도구가 보는 사용자와 같은지 assert합니다.
요약
- SEP-990은 최종 사용자가 아니라 엔터프라이즈 ID 공급자가 클라이언트가 어느 MCP 서버에 접근할 수 있는지를 결정하게 합니다. IdP는 그 결정을 ID-JAG에 서명해 담습니다.
- ID-JAG를 얻는 것은 자체 IdP를 상대로 한 RFC 8693 토큰 교환이며, SDK는 이를 수행하지 않습니다. ID-JAG를 MCP 인가 서버에 제시하는 것은 RFC 7523
jwt-bearer그랜트이며, SDK는 그 양쪽을 모두 수행합니다. IdentityAssertionOAuthProvider는 또 하나의httpx2.Auth입니다. 사전 등록된 기밀 클라이언트, 고정된issuer, 그리고assertion_provider(audience, resource)콜백 하나로 이루어집니다. 브라우저도, 등록도, 리프레시 토큰도 없습니다.- 인가 서버를 리소스 서버를 통해 찾아내는 일은 없습니다.
issuer는 메타데이터 문서가 내주는 문자열과 정확히 같게 설정하세요. 비교는 한 글자씩 이루어집니다. - 서버 쪽에서는
identity_assertion_enabled=True에exchange_identity_assertion을 더합니다. SDK는 클라이언트를 인증하고 그랜트의 관문을 지키며, ID-JAG 검증은 전적으로 직접 구현할 몫이고, 발급되는 토큰은 요청의 것이 아니라 ID-JAG의resource에 묶입니다.
이 페이지가 한 번도 건드리지 않은 당사자는 MCP 서버입니다. 방금 발급한 토큰으로 MCP 서버가 하는 일은 이미 인가에서 하던 일입니다.