|
4 | 4 | - ClientCredentialsOAuthProvider: For client_credentials with client_id + client_secret |
5 | 5 | - PrivateKeyJWTOAuthProvider: For client_credentials with private_key_jwt authentication |
6 | 6 | (typically using a pre-built JWT from workload identity federation) |
7 | | -- RFC7523OAuthClientProvider: For jwt-bearer grant (RFC 7523 Section 2.1) |
8 | 7 | """ |
9 | 8 |
|
10 | 9 | import time |
11 | | -import warnings |
12 | 10 | from collections.abc import Awaitable, Callable |
13 | 11 | from typing import Any, Literal |
14 | 12 | from uuid import uuid4 |
|
17 | 15 | import jwt |
18 | 16 | from pydantic import BaseModel, Field |
19 | 17 |
|
20 | | -from mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthTokenError, TokenStorage |
21 | | -from mcp.shared.auth import AuthorizationCodeResult, OAuthClientInformationFull, OAuthClientMetadata |
22 | | -from mcp.shared.exceptions import MCPDeprecationWarning |
| 18 | +from mcp.client.auth import OAuthClientProvider, OAuthFlowError, TokenStorage |
| 19 | +from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata |
23 | 20 |
|
24 | 21 |
|
25 | 22 | class ClientCredentialsOAuthProvider(OAuthClientProvider): |
@@ -334,153 +331,3 @@ async def _exchange_token_client_credentials(self) -> httpx2.Request: |
334 | 331 |
|
335 | 332 | token_url = self._get_token_endpoint() |
336 | 333 | return httpx2.Request("POST", token_url, data=token_data, headers=headers) |
337 | | - |
338 | | - |
339 | | -class JWTParameters(BaseModel): |
340 | | - """JWT parameters.""" |
341 | | - |
342 | | - assertion: str | None = Field( |
343 | | - default=None, |
344 | | - description="JWT assertion for JWT authentication. " |
345 | | - "Will be used instead of generating a new assertion if provided.", |
346 | | - ) |
347 | | - |
348 | | - issuer: str | None = Field(default=None, description="Issuer for JWT assertions.") |
349 | | - subject: str | None = Field(default=None, description="Subject identifier for JWT assertions.") |
350 | | - audience: str | None = Field(default=None, description="Audience for JWT assertions.") |
351 | | - claims: dict[str, Any] | None = Field(default=None, description="Additional claims for JWT assertions.") |
352 | | - jwt_signing_algorithm: str | None = Field(default="RS256", description="Algorithm for signing JWT assertions.") |
353 | | - jwt_signing_key: str | None = Field(default=None, description="Private key for JWT signing.") |
354 | | - jwt_lifetime_seconds: int = Field(default=300, description="Lifetime of generated JWT in seconds.") |
355 | | - |
356 | | - def to_assertion(self, with_audience_fallback: str | None = None) -> str: |
357 | | - if self.assertion is not None: |
358 | | - # Prebuilt JWT (e.g. acquired out-of-band) |
359 | | - assertion = self.assertion |
360 | | - else: |
361 | | - if not self.jwt_signing_key: |
362 | | - raise OAuthFlowError("Missing signing key for JWT bearer grant") # pragma: no cover |
363 | | - if not self.issuer: |
364 | | - raise OAuthFlowError("Missing issuer for JWT bearer grant") # pragma: no cover |
365 | | - if not self.subject: |
366 | | - raise OAuthFlowError("Missing subject for JWT bearer grant") # pragma: no cover |
367 | | - |
368 | | - audience = self.audience if self.audience else with_audience_fallback |
369 | | - if not audience: |
370 | | - raise OAuthFlowError("Missing audience for JWT bearer grant") # pragma: no cover |
371 | | - |
372 | | - now = int(time.time()) |
373 | | - claims: dict[str, Any] = { |
374 | | - "iss": self.issuer, |
375 | | - "sub": self.subject, |
376 | | - "aud": audience, |
377 | | - "exp": now + self.jwt_lifetime_seconds, |
378 | | - "iat": now, |
379 | | - "jti": str(uuid4()), |
380 | | - } |
381 | | - claims.update(self.claims or {}) |
382 | | - |
383 | | - assertion = jwt.encode( |
384 | | - claims, |
385 | | - self.jwt_signing_key, |
386 | | - algorithm=self.jwt_signing_algorithm or "RS256", |
387 | | - ) |
388 | | - return assertion |
389 | | - |
390 | | - |
391 | | -class RFC7523OAuthClientProvider(OAuthClientProvider): |
392 | | - """OAuth client provider for RFC 7523 jwt-bearer grant. |
393 | | -
|
394 | | - .. deprecated:: |
395 | | - Use :class:`ClientCredentialsOAuthProvider` for client_credentials with |
396 | | - client_id + client_secret, or :class:`PrivateKeyJWTOAuthProvider` for |
397 | | - client_credentials with private_key_jwt authentication instead. |
398 | | -
|
399 | | - This provider supports the jwt-bearer authorization grant (RFC 7523 Section 2.1) |
400 | | - where the JWT itself is the authorization grant. |
401 | | - """ |
402 | | - |
403 | | - def __init__( |
404 | | - self, |
405 | | - server_url: str, |
406 | | - client_metadata: OAuthClientMetadata, |
407 | | - storage: TokenStorage, |
408 | | - redirect_handler: Callable[[str], Awaitable[None]] | None = None, |
409 | | - callback_handler: Callable[[], Awaitable[AuthorizationCodeResult]] | None = None, |
410 | | - timeout: float = 300.0, |
411 | | - jwt_parameters: JWTParameters | None = None, |
412 | | - ) -> None: |
413 | | - warnings.warn( |
414 | | - "RFC7523OAuthClientProvider is deprecated. Use ClientCredentialsOAuthProvider " |
415 | | - "or PrivateKeyJWTOAuthProvider instead.", |
416 | | - MCPDeprecationWarning, |
417 | | - stacklevel=2, |
418 | | - ) |
419 | | - super().__init__(server_url, client_metadata, storage, redirect_handler, callback_handler, timeout) |
420 | | - self.jwt_parameters = jwt_parameters |
421 | | - |
422 | | - async def _exchange_token_authorization_code( |
423 | | - self, auth_code: str, code_verifier: str, *, token_data: dict[str, Any] | None = None |
424 | | - ) -> httpx2.Request: # pragma: no cover |
425 | | - """Build token exchange request for authorization_code flow.""" |
426 | | - token_data = token_data or {} |
427 | | - if self.context.client_metadata.token_endpoint_auth_method == "private_key_jwt": |
428 | | - self._add_client_authentication_jwt(token_data=token_data) |
429 | | - return await super()._exchange_token_authorization_code(auth_code, code_verifier, token_data=token_data) |
430 | | - |
431 | | - async def _perform_authorization(self) -> httpx2.Request: # pragma: no cover |
432 | | - """Perform the authorization flow.""" |
433 | | - if "urn:ietf:params:oauth:grant-type:jwt-bearer" in self.context.client_metadata.grant_types: |
434 | | - token_request = await self._exchange_token_jwt_bearer() |
435 | | - return token_request |
436 | | - else: |
437 | | - return await super()._perform_authorization() |
438 | | - |
439 | | - def _add_client_authentication_jwt(self, *, token_data: dict[str, Any]): # pragma: no cover |
440 | | - """Add JWT assertion for client authentication to token endpoint parameters.""" |
441 | | - if not self.jwt_parameters: |
442 | | - raise OAuthTokenError("Missing JWT parameters for private_key_jwt flow") |
443 | | - if not self.context.oauth_metadata: |
444 | | - raise OAuthTokenError("Missing OAuth metadata for private_key_jwt flow") |
445 | | - |
446 | | - # We need to set the audience to the issuer identifier of the authorization server |
447 | | - # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523 |
448 | | - issuer = str(self.context.oauth_metadata.issuer) |
449 | | - assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer) |
450 | | - |
451 | | - # When using private_key_jwt, in a client_credentials flow, we use RFC 7523 Section 2.2 |
452 | | - token_data["client_assertion"] = assertion |
453 | | - token_data["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" |
454 | | - # We need to set the audience to the resource server, the audience is different from the one in claims |
455 | | - # it represents the resource server that will validate the token |
456 | | - token_data["audience"] = self.context.get_resource_url() |
457 | | - |
458 | | - async def _exchange_token_jwt_bearer(self) -> httpx2.Request: |
459 | | - """Build token exchange request for JWT bearer grant.""" |
460 | | - if not self.context.client_info: |
461 | | - raise OAuthFlowError("Missing client info") # pragma: no cover |
462 | | - if not self.jwt_parameters: |
463 | | - raise OAuthFlowError("Missing JWT parameters") # pragma: no cover |
464 | | - if not self.context.oauth_metadata: |
465 | | - raise OAuthTokenError("Missing OAuth metadata") # pragma: no cover |
466 | | - |
467 | | - # We need to set the audience to the issuer identifier of the authorization server |
468 | | - # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523 |
469 | | - issuer = str(self.context.oauth_metadata.issuer) |
470 | | - assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer) |
471 | | - |
472 | | - token_data = { |
473 | | - "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", |
474 | | - "assertion": assertion, |
475 | | - } |
476 | | - |
477 | | - if self.context.should_include_resource_param(self.context.protocol_version): # pragma: no branch |
478 | | - token_data["resource"] = self.context.get_resource_url() |
479 | | - |
480 | | - if self.context.client_metadata.scope: # pragma: no branch |
481 | | - token_data["scope"] = self.context.client_metadata.scope |
482 | | - |
483 | | - token_url = self._get_token_endpoint() |
484 | | - return httpx2.Request( |
485 | | - "POST", token_url, data=token_data, headers={"Content-Type": "application/x-www-form-urlencoded"} |
486 | | - ) |
0 commit comments