Skip to content

Phase 2: OAuth 2.1 RFC compliance - #13

Open
manish-wekan wants to merge 3 commits into
developfrom
phase-2-oauth-compliance
Open

Phase 2: OAuth 2.1 RFC compliance#13
manish-wekan wants to merge 3 commits into
developfrom
phase-2-oauth-compliance

Conversation

@manish-wekan

@manish-wekan manish-wekan commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Closes the RFC-compliance gaps in nitrostack/auth/oauth.py tracked as Phase 2 of the modernization plan.

Added

  • PKCE (RFC 7636)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.
  • RFC 8707 resource-indicator (audience) validation — a token's aud claim 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.
  • Full RFC 8414 discovery document/.well-known/oauth-authorization-server now serves all commonly-required fields instead of 3.
  • Dynamic Client Registration (RFC 7591)nitrostack/auth/oauth_module.py, a POST /oauth/v2/register endpoint. This is the same simplified, static-credential variant the TypeScript SDK ships (opt-in, hands back pre-configured client_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.
  • JWKS client caching and token-result caching (TTL-based) — every tool call previously reconstructed the JWKS client and re-verified from scratch; now both are cached, matching the TypeScript SDK's caching strategy.
  • AuthContext.aud — surfaces the audience claim to guard/tool code, normalized to a list.

Changed

  • Introspection-endpoint-before-JWKS precedence now matches TS exactly (previously reversed) — a deployment configuring both no longer gets divergent behavior between the two SDKs.
  • Removed the mock-active fallback. Previously, an OAuthService configured with neither JWKS_URI nor OAUTH_INTROSPECTION_ENDPOINT treated any bearer token as valid — a misconfigured production deployment would silently accept everything. It now returns active: 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.py grows 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.py still imports and executes cleanly, matching its pre-change baseline. Manually verified the discovery server's actual wire output with curl against /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource.

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.
@hemantj-cloud

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants