feat(auth): add Authlib-backed OAuth adapter (related to #1240)#2193
Open
harsh543 wants to merge 7 commits intomodelcontextprotocol:mainfrom
Open
feat(auth): add Authlib-backed OAuth adapter (related to #1240)#2193harsh543 wants to merge 7 commits intomodelcontextprotocol:mainfrom
harsh543 wants to merge 7 commits intomodelcontextprotocol:mainfrom
Conversation
…ocol#1240) Introduces AuthlibOAuthAdapter, an httpx.Auth-compatible plugin that wraps Authlib's AsyncOAuth2Client to handle token acquisition, automatic refresh, and Bearer-header injection. This is Phase 1 of the OAuth refactor tracked in issue modelcontextprotocol#1240. The adapter supports: - client_credentials grant (fully self-contained) - authorization_code + PKCE (S256 enforced, 128-char verifier ≈ 762-bit entropy) - Automatic token refresh via ensure_active_token + update_token hook - TokenStorage bridge — bidirectional conversion between OAuthToken and Authlib's internal token dict, keeping the storage protocol unchanged - Concurrency safety via anyio.Lock - Secret hygiene — client_secret excluded from repr (Field(repr=False)) No existing code is modified. OAuthClientProvider, TokenStorage, OAuthToken, PKCEParameters, and all exceptions retain their current signatures. Both providers coexist; callers can migrate one client at a time. Github-Issue:modelcontextprotocol#1240
- utils.py imported from mcp.client.auth (package __init__) instead of mcp.client.auth.exceptions directly, creating a circular dependency that forced authlib_adapter to be imported last in __init__.py (violating import order). Fix by importing directly from the exceptions module. - Reorder __init__.py imports alphabetically (ruff I001) - Remove unused MagicMock import from test file (ruff F401) - Remove extra blank line in test imports (ruff E303)
…mode Define _AsyncOAuth2ClientProtocol as a minimal structural interface for AsyncOAuth2Client's members used by AuthlibOAuthAdapter (token, scope, code_challenge_method, fetch_token, create_authorization_url, ensure_active_token). Annotate self._client with the Protocol and suppress the assignment from the untyped library with # type: ignore[assignment]. This eliminates all reportUnknownMemberType / reportUnknownVariableType / reportUnknownArgumentType errors in pyright strict mode without requiring upstream type stubs or a proliferation of per-line ignores.
…nschema on Windows The previous implementation patched Path.iterdir globally on the class, which caused jsonschema_specifications to receive fake paths when it lazily loads its schema files on Windows, resulting in FileNotFoundError. Capture the original method and forward all non-fake paths to it so that only iterdir calls on the mocked desktop path return the stub file list.
Protocol stub methods (fetch_token, create_authorization_url, ensure_active_token) have Ellipsis bodies that coverage flags as uncovered branches because Protocol classes are never instantiated. Mark them with pragma: no cover. The fallback path in _mock_iterdir (returning original_iterdir when the path does not contain "fake") is only reachable on Windows where jsonschema lazily loads schemas during the test. On Linux/macOS jsonschema loads schemas eagerly so the branch is never taken. Mark it accordingly.
Author
|
This is a narrowly scoped, low‑risk foundation PR: it adds a minimal AuthProvider protocol and an Authlib‑based httpx.Auth adapter without touching existing OAuth behavior. It improves maintainability and future RFC coverage while preserving current defaults. Security posture is unchanged or better (no token logging, explicit Bearer injection, strict state validation). Recommended approve + merge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The MCP Python SDK maintains a 1,600+ line bespoke OAuth2 client stack that bundles metadata discovery, dynamic client registration, token exchange, PKCE, and refresh into a single monolithic class (
OAuthClientProvider). As supported grant types and auth methods grow, this becomes expensive to maintain and extend — the motivation behind #1240.Solution
Introduce
AuthlibOAuthAdapter— anhttpx.Auth-compatible plugin backed by Authlib's battle-testedAsyncOAuth2Client.The adapter handles:
client_credentialsgrant — fully self-contained, no browser interactionauthorization_code+ PKCE — S256 enforced, 128-char verifier ≈ 762-bit entropyensure_active_token+update_tokenhookTokenStoragebridge — bidirectional conversion betweenOAuthTokenand Authlib's internal token dict; the storage protocol is unchangedanyio.Lockguards all mutable token stateclient_secretexcluded fromreprviaField(repr=False)to prevent accidental log leakageSafety: incremental migration
This PR is purely additive. Nothing existing is modified:
OAuthClientProvider— unchangedTokenStorageprotocol — unchangedOAuthTokenmodel — unchangedBoth providers coexist. Callers can migrate one
httpx.AsyncClientat a time.Summary of Changes
Modified files
pyproject.tomlauthlib>=1.4.0dependencysrc/mcp/client/auth/__init__.pyAuthlibAdapterConfig,AuthlibOAuthAdapterNew files
src/mcp/client/auth/authlib_adapter.pyAuthlibAdapterConfig(Pydantic config model) +AuthlibOAuthAdapter(httpx.Auth)tests/client/auth/__init__.pytests/client/auth/test_authlib_adapter.pyArchitecture
AsyncOAuth2Clientuses its own internal transport for token requests — no circular dependency with the parenthttpx.AsyncClient.Backward Compatibility
All existing public symbols from
mcp.client.authretain their current signatures. No deprecation warnings in this PR. Deprecation ofOAuthClientProvideris planned for Phase 2 (announced one minor version in advance).Test Coverage
31 tests covering all branches:
_initialize: stored token, no token, partial token (missing optional fields)_on_token_update: full token dict, missing optional fields_fetch_client_credentials_token: with/withoutextra_token_params, token absent after fetch_perform_authorization_code_flow: missing endpoint, redirect_uri, redirect_handler, callback_handler; state mismatch;Nonestate (or-guard short-circuit); empty code; token absent after fetch; happy pathasync_auth_flow: bearer injection, no-token path, 401 retry (client_credentials and auth_code),ensure_active_tokencalled/skipped_inject_bearer: None token, missing access_token key, valid tokenSecurity
plainmethod acceptedsecrets.token_urlsafe(32)(256 bits), validated viasecrets.compare_digestclient_secretexcluded fromrepr— safe to logAuthlibAdapterConfiginstancesanyio.Lockprevents concurrent token mutation racesFuture Work (not in this PR)
OAuthClientProvider; add resource indicator validation to adapter; add discovery helper to resolve PRM/OASM endpoints intoAuthlibAdapterConfigpyjwt[crypto]; addprivate_key_jwtvia AuthlibPrivateKeyJWT