Phase 2: OAuth 2.1 RFC compliance - #13
Open
manish-wekan wants to merge 3 commits into
Open
Conversation
Add PKCE (RFC 7636) as a standalone, fully-tested utility module — not wired into a local authorization-code exchange endpoint, since nitrostack is an OAuth resource server and never issues tokens itself in either SDK. Expand OAuthService: cache JWKS clients and introspection results (TTL-based), check the introspection endpoint before JWKS to match the TypeScript SDK's precedence, and validate the token's audience claim (RFC 8707) on both validation paths instead of only the JWKS one. Remove the fallback that treated any bearer token as valid when neither JWKS nor an introspection endpoint was configured. Unconfigured now means reject, matching the TypeScript reference, which has no such fallback. Expand the discovery server: full RFC 8414 authorization-server metadata instead of a 3-field stub, and a Dynamic Client Registration (RFC 7591) endpoint using the same simplified static-credential variant the TypeScript SDK ships — opt-in, hands back pre-configured credentials, no per-client storage. Add AuthContext.aud so guard code can see which audience a token was issued for, normalized to a list since the claim is legal as either a string or a list. tests/test_oauth.py grows from 1 test to 19, including a named regression test for the removed mock-fallback and coverage for the new caches, audience checks, discovery document shape, and registration gating.
Collaborator
|
I’m adding the OAuth credentials here only for testing the Python SDK OAuth flow. These credentials are most likely expired, or the cluster may be scaled down. You can create new credentials by following the OAuth blog: https://nitrostack.ai/blog/securing-mcp-with-auth0-and-oauth-2-1-a-complete-step-by-step-guide The configuration includes everything required to test OAuth 2.1 MCP authentication, including the Auth0 server, JWKS, audience, issuer, client ID, and client secret, same as the TypeScript SDK. Please also check that all required environment variables are covered in the Python SDK. # OAuth 2.1 MCP Server Configuration
# =============================================================================
# Enforcement Gate (Optional)
# =============================================================================
# OAUTH_REQUIRED controls whether authentication is enforced.
# - Unset / false (default): auth is NOT enforced. The server runs out-of-the-box
# and protected endpoints are reachable without a token. Best for local dev.
# - true: auth is enforced (fail-closed). If neither JWKS_URI nor
# INTROSPECTION_ENDPOINT is configured, the server STILL starts and logs a
# clear warning, but rejects all protected requests until a verifier is set.
# Set this to true (and configure a verifier below) before deploying.
# =============================================================================
OAUTH_REQUIRED=true
# =============================================================================
# REQUIRED: Server Configuration
# =============================================================================
# Your Auth0 API Identifier (from APIs → MCP Server → Identifier)
# This MUST match exactly what you set when creating the API
RESOURCE_URI=http://localhost:3000/mcp
# Your Auth0 tenant domain (authorization server URL)
AUTH_SERVER_URL=https://dev-q5ckeg6fevwjztcc.us.auth0.com
# Cryptographic JWKS Signature Verification (Recommended)
JWKS_URI=https://dev-q5ckeg6fevwjztcc.us.auth0.com/.well-known/jwks.json
# Expected token validation audience (should match RESOURCE_URI)
TOKEN_AUDIENCE=http://localhost:3000/mcp
# Expected token validation issuer (your authorization server tenant domain)
TOKEN_ISSUER=https://dev-q5ckeg6fevwjztcc.us.auth0.com/
# Dynamic Client Registration (Optional / disabled by default)
# =============================================================================
# Set to 'true' to expose the /oauth/v2/register endpoint. It only serves the
# statically configured client below; without OAUTH_CLIENT_ID it stays disabled.
# =============================================================================
OAUTH_ENABLE_CLIENT_REGISTRATION=true
OAUTH_CLIENT_ID=JxWKcvFsJxjtxjjlzWFSok3zKCT6I5Rd
OAUTH_CLIENT_SECRET=1SucwbUIdgoy3hXMSiMMcC6VC9BOyQoBRf_LAtaeIjLKW15rngw47SDY4jrLZ234 |
Resolves a conflict in OAuthGuard.can_activate: develop (via #12) added the OAUTH_REQUIRED enforcement gate, this branch added AuthContext.aud normalization. Both are kept -- the gate decides whether a request proceeds, the normalization shapes the AuthContext once it does.
…h env set The setup docs (OAUTH_SETUP.md, the CLI's generated guide, and the flight booking example) all tell users to set OAUTH_INTROSPECTION_ENDPOINT, but the generated app modules read INTROSPECTION_ENDPOINT. Following the documentation therefore left token_introspection_endpoint as None, so introspection was never configured. Before the mock-fallback removal that silently accepted every token; after it, every token is rejected -- so the same latent bug now presents as auth being broken rather than absent. OAuthService now resolves the endpoint from either spelling, preferring OAUTH_INTROSPECTION_ENDPOINT, and also falls back to INTROSPECTION_CLIENT_ID / INTROSPECTION_CLIENT_SECRET. Resolving it in the service rather than in each template fixes every caller at once, including app modules already written against either name. Explicit constructor arguments still win over the environment. The TypeScript SDK reads both variables too. Expands the OAuth setup guide's .env block into a complete reference covering every variable the SDK actually reads: the OAUTH_REQUIRED enforcement gate, server identity, both verification methods, claim validation, the RFC 7591 registration settings, and the cache/discovery-port tuning knobs. Several of these were supported but undocumented. Three regression tests; two of them fail without this change.
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.
Closes the RFC-compliance gaps in
nitrostack/auth/oauth.pytracked as Phase 2 of the modernization plan.Added
nitrostack/auth/pkce.py, a standalone, fully-tested utility module (verifier/challenge generation, verification, format validation). Not wired into a local authorization-code exchange endpoint: nitrostack is an OAuth resource server, never an authorization server, in either SDK — it validates incoming tokens but never issues them. The TypeScript reference confirms this scope.audclaim is now checked against the configured resource on both validation paths. Previously only the JWKS path enforced this (via PyJWT internally); the introspection-endpoint path had no check at all, meaning a token issued for a different resource would have been silently accepted./.well-known/oauth-authorization-servernow serves all commonly-required fields instead of 3.nitrostack/auth/oauth_module.py, aPOST /oauth/v2/registerendpoint. This is the same simplified, static-credential variant the TypeScript SDK ships (opt-in, hands back pre-configuredclient_id/client_secret, no per-client storage), not full multi-tenant DCR — matches what TS actually does, not the plan file's more elaborate wording.AuthContext.aud— surfaces the audience claim to guard/tool code, normalized to a list.Changed
OAuthServiceconfigured with neitherJWKS_URInorOAUTH_INTROSPECTION_ENDPOINTtreated any bearer token as valid — a misconfigured production deployment would silently accept everything. It now returnsactive: False, matching the TypeScript SDK, which has no such fallback. Has its own named regression test (test_unconfigured_service_rejects_by_default) so this can't silently regress back.Testing
tests/test_oauth.pygrows from 1 test to 19 (pytest tests/test_oauth.py -v— all passing), covering PKCE round-trips and boundary cases, audience validation on both paths, the mock-fallback regression, RFC 8414 required-field coverage, DCR gating (disabled by default, requires both an opt-in flag and a configured client id), and both caches (JWKS client construction count, token-result reuse, TTL expiry).Full suite (
test_basic,test_tasks— 45 passed,test_initial_tool,test_production,test_transports,test_widget_metadata,test_tool_input_schema,test_cli— 59 passed) — zero regressions.examples/flight_booking_server.pystill imports and executes cleanly, matching its pre-change baseline. Manually verified the discovery server's actual wire output withcurlagainst/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource.