Refactor/opt in surfaces - #3
Open
ShivSankalp wants to merge 3 commits into
Open
Conversation
NamoID is the authorization server, the customer's MCP server is the protected resource, and an MCP host is the OAuth client. This covers the resource-server half: RFC 9728 metadata, audience-bound token verification, and per-tool scope enforcement. No NamoID credentials are needed, because a resource server only consumes public discovery metadata and JWKS. namoid.mcp imports no MCP framework, so it also backs the official MCP Python SDK, a bare Starlette app, or a test. The framework appears only in namoid.mcp.fastmcp. Both arrive as extras so the base install is unaffected. The FastMCP adapter wraps the shared core rather than subclassing FastMCP's JWTVerifier, which avoids two failure modes by construction: - JWTVerifier has no opinion on token_use, so an ID token for the same audience would be accepted as an MCP API token. - JWTVerifier leaves AccessToken.subject unset, which would make every caller look like one anonymous principal and silently apply per-user checks to a shared identity. require_namoid_scopes is preferred over FastMCP's require_scopes, which filters a tool out of tools/list rather than challenging, leaving the host unable to ask the user to approve it. Incremental authorization needs the tool to stay visible and answer with insufficient_scope. The provider also publishes the issuer verbatim. RemoteAuthProvider stores authorization servers as pydantic AnyHttpUrl, which appends a trailing slash to a bare authority; RFC 8414 compares issuer identifiers exactly, so a strict client would see a mismatch. Also caches JWKS with refetch on an unrecognised kid, bounded by a cooldown so invented key IDs cannot be turned into traffic against the issuer, and adds a CI workflow, which this repository had none of.
Hosted Auth redirects the user to a branded NamoID sign-in page and returns a one-time code. The Client ID resolves the application, its environment, and its Hosted Auth domain, so there is no issuer or application UUID to configure. NamoIDClient and AsyncNamoIDClient cover the whole flow: auth config, PKCE transactions, hosted URL construction, code exchange, refresh, server-side token validation, and session revocation. Both share one definition of every request and one response parser, so the sync and async surfaces cannot drift apart. Pure pieces live in namoid.hosted_auth, so an application that wants to drive the redirect itself does not have to construct a client. Every endpoint and payload shape was taken from the running service rather than assumed, which surfaced three fields the JavaScript SDK does not model: user_id on the token response, support_email on the auth config, and the /v1/auth/refresh endpoint, which has no method in @namoidhq/js even though an example relies on it. httpx moves from the extras to a package dependency, since making HTTP requests is now the package's core job rather than something only the MCP extra needed. Failures raise NamoIDError carrying status, the API's own error code where one is returned, and the parsed detail.
Importing namoid loaded the Hosted Auth client eagerly, so an MCP-only server paid for code it never asked for. The top-level names now resolve on first use (PEP 562), which gives three guarantees: - Importing namoid loads neither surface, and neither httpx nor joserfc. - A Hosted Auth application never imports the MCP code or needs its extra. - An MCP server never imports the Hosted Auth client. dir() and __all__ still list the full surface and a TYPE_CHECKING block keeps every export resolvable, so laziness costs nothing in editor or type-checker support. The MCP errors now subclass NamoIDError, so one except catches both surfaces. The error module is stdlib-only, so sharing it couples no dependency, and the message a resource server surfaces is unchanged. tests/test_modularity.py enforces the boundaries rather than leaving them as an intention. Each test runs in a fresh interpreter, because sys.modules is process-global and an earlier import in the session would mask exactly what is being checked. The strictest one replaces builtins.__import__ and fails if the MCP core so much as touches fastmcp, starlette, mcp, or the Hosted Auth modules. This also fixed the same coupling in the test suite itself: conftest imported joserfc at module scope, so a base-only install could not even collect the Hosted Auth tests. The suite now degrades by install: 31 tests on the base, 59 with the mcp extra, 73 with fastmcp.
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.
What does this change?
Related issue
Checklist