Commit graph

231 commits

Author SHA1 Message Date
nate nowack
75fb116e36
Support EdDSA verification in JWTVerifier (#4752) 2026-08-06 14:09:37 -04:00
Eduardo Cruz Guedes
794bfe9567
Add valid_scopes parameter to OIDC proxy valid scopes (#4660)
* Accept valid_scopes on OIDCProxy

* Keep valid_scopes when verify_id_token restores scopes

* Document valid_scopes on the OIDC proxy

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-27 15:12:15 -04:00
Jeremiah Lowin
95edc1d2f3
Record effective scopes on the OAuth transaction (#4670) 2026-07-27 14:33:57 -04:00
Jeremiah Lowin
920cb47778
Add require_roles auth check (#4656)
* Add require_roles auth check

* Make role docs runnable standalone and fully annotated

* Treat a scalar role claim as one role; correct step-up docs

* Add v4 version badge to require_roles docs
2026-07-27 12:36:58 -04:00
Jeremiah Lowin
0c1c42f151
Bind CIMD assertion audience to the advertised token endpoint (#4659)
A bare-authority base_url renders with a trailing slash, so the expected aud was https://host//token while the metadata advertised https://host/token.
2026-07-27 11:46:03 -04:00
Jeremiah Lowin
62afdca775
Bind ID-JAG audience to the issuer identifier 2026-07-27 09:37:38 -04:00
Jeremiah Lowin
98ac0402df
Keep ID-JAG audience on base_url, out of scope for issuer identity 2026-07-26 17:48:01 -04:00
Jeremiah Lowin
a7248480e2
Make InMemoryOAuthProvider keyword-only like its parent 2026-07-26 15:34:06 -04:00
Jeremiah Lowin
681d5a7120
Apply ruff format to issuer identity tests 2026-07-26 15:12:50 -04:00
Jeremiah Lowin
c4c72ac240
Use issuer_url for OAuth issuer identity, not base_url 2026-07-26 15:10:15 -04:00
Jeremiah Lowin
b07f9ce9ae
Honor OAuth application_type in DCR (SEP-837) (#4621)
* Honor OAuth application_type in DCR (SEP-837)

* Simplify web redirect check per ruff SIM103

* Enforce application_type over HTTP, at auth time, and tighten native scheme rules

Recover the DCR application_type the SDK RegistrationHandler drops (P1), enforce the stored type on the authorization redirect path (P2), restrict native to loopback http + custom schemes (P2), and document the web/native rules (P2).

* Fix loopback range detection and use a positive scheme allowlist

Classify loopback hosts with ipaddress.is_loopback so all of 127.0.0.0/8 counts (a web client could bypass the non-loopback rule with 127.0.0.2). Replace the NON_REDIRECT_NETWORK_SCHEMES denylist with STANDARD_URI_SCHEMES: native now accepts only https, loopback http, and unregistered private-use schemes per RFC 8252, so smb/smtp/nfs and other unlisted standard schemes no longer pass.

* Vendor the IANA scheme registry and consolidate the loopback classifier

Replace the hand-picked STANDARD_URI_SCHEMES with a vendored snapshot of the IANA URI scheme registry (423 schemes), so registered transports nobody enumerated (coap, coaps, stun, turn, mqtt) fail closed instead of passing as private-use. Delete the stale duplicate _is_loopback_host in oauth_proxy/models.py and reuse the ipaddress-based classifier from redirect_validation, restoring loopback port flexibility across all of 127.0.0.0/8.

* Treat the reserved localhost namespace and absolute host forms as loopback

RFC 6761 6.3 reserves the whole localhost namespace for the local machine, so app.localhost and localhost. are loopback just as much as localhost. Previously a web client could register https://app.localhost/callback and bypass the non-loopback rule, while native clients were wrongly refused legitimate http://app.localhost:3000 dev callbacks. The suffix test is anchored on a leading dot so localhost.evil.com and notlocalhost stay non-loopback.

* Narrow scope: drop native scheme classification, keep the web rule

Registry membership cannot separate app-dispatch schemes from network transports (vscode is registered because it is an app scheme), so classifying a native client's scheme rejected callbacks that real MCP clients need. Remove the vendored registry and the private-use test; native now accepts any scheme outside the unsafe set, with cleartext http still limited to loopback. Also reject web registrations that omit redirect_uris rather than storing an unusable localhost placeholder.
2026-07-26 14:24:50 -04:00
Jeremiah Lowin
4ebb3fd5e6
Emit scope step-up challenges for incremental authorization (SEP-2350) (#4623)
* Signal component-level scope shortfalls as insufficient_scope (SEP-2350)

* Fix ty type narrowing in scope step-up test

* Respect check short-circuit when reporting scope shortfall (P2)

* Report union of unmet scopes and document step-up contract

* Aggregate scope shortfall across the AuthMiddleware chain

* Stop chain scope aggregation at the first unevaluated gate
2026-07-26 14:13:26 -04:00
Jeremiah Lowin
fbee629ed9
Advertise only supported token endpoint auth methods in OAuthProxy metadata (#4608)
* Advertise only supported token endpoint auth methods in OAuthProxy metadata

* Normalize DCR registration response to the stored public client
2026-07-23 18:11:18 -04:00
Jeremiah Lowin
1a788cf349
Merge remote-tracking branch 'origin/main' into feature/client-auto-default
# Conflicts:
#	tests/client/test_streamable_http.py
#	tests/server/middleware/test_initialization_middleware.py
#	tests/server/tasks/test_task_status_notifications.py
2026-07-20 12:02:38 -04:00
Jeremiah Lowin
b0e782a2ee
Make the unit suite fast: in-process HTTP tests, no real sleeps, parallel Windows CI (#4554) 2026-07-20 10:51:14 -04:00
Jeremiah Lowin
d2ac7ed3d2
Default fastmcp.Client to mode="auto"; surface extensions=/result_claims=
Client negotiates the newest mutual protocol era by default (probe
server/discover, fall back to the initialize handshake). ProxyClient and the
inspect utility explicitly pin the handshake era so proxy forwarding and
server_info reads are unchanged. SSE and multi-server config transports are
legacy-only. extensions= and result_claims= (SEP-2133) are thin passthroughs
to the SDK session.
2026-07-19 21:22:49 -04:00
Jeremiah Lowin
b9b1deacb6
Speed up the unit test suite, and fix the task-notification race it surfaced (#4550) 2026-07-19 18:52:04 -04:00
Jeremiah Lowin
67e8448389
[codex] Add OAuthProxy RFC 9207 issuer responses (#4438)
* Add OAuthProxy issuer response parameter

* Cover OAuthProxy issuer error redirects

* Relax host origin guard defaults (#4439)

* Use exact issuer in authorize errors

* Restore HTTP host guard compatibility (#4472)

* Hugging Face Auth Integration (#4385)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>

* Docs: add v3.4.4 changelog entries (#4473)

* Explain unnormalized issuer; cover consent-denial path base_url

* Revert "Merge remote-tracking branch 'origin/release/3.x' into codex/oauth-proxy-rfc9207-issuer"

This reverts commit 9e34b1686c, reversing
changes made to 640dc60fe0.

* Preserve callback query bytes when appending iss/code/state params

add_query_params previously decoded the existing query with parse_qsl
and re-encoded it, mutating opaque or signed query strings (a valueless
?flag became ?flag=, non-UTF-8 percent-encoded bytes got replaced).
Append the newly-encoded params to the existing query string instead of
round-tripping it through parse/encode.

Also fixes a stray bare `httpx` reference in a test that should use
httpx2 following the SDK v2 migration.

* Attach RFC 9207 iss to authorize() success redirects too

AuthorizationHandler only added iss to error redirects from the SDK's
base handler, not to code redirects returned directly by authorize()
overrides that bypass consent/upstream (as GitHub's mocked test does).
Since metadata now unconditionally advertises
authorization_response_iss_parameter_supported, any client-facing
redirect missing iss hard-fails RFC 9207-aware clients.

Also fixes HeadlessOAuth, which parsed code/state from the redirect
but silently dropped iss, so the same regression would have masked
itself across every other provider integration test too.

* Carry RFC 9207 iss through the production OAuth callback path

OAuthProxy advertises authorization_response_iss_parameter_supported and
sends iss on every authorization redirect, but the client's production
callback chain (CallbackResponse -> OAuthCallbackResult -> OAuth.callback_handler)
had no iss field, so it was silently dropped and the SDK's
validate_authorization_response_iss rejected the callback. HeadlessOAuth
already carried iss through, which is why CI stayed green while real
clients failed.

Add iss to CallbackResponse and OAuthCallbackResult, thread it through
store_result_once for both success and error branches, and pass it into
AuthorizationCodeResult in OAuth.callback_handler.

* Don't duplicate iss when a provider redirect already carries one

* Consolidate RFC 9207 iss handling into a single redirect helper

Every client-facing authorization redirect must carry exactly one iss.
That invariant was being enforced by hand at five separate call sites,
each building its own params dict -- which is how the success-redirect
path shipped without iss in the first place, and how a registered
redirect_uri that already carries its own iss could end up duplicated.
Route all five sites through build_client_redirect(), which owns the
idempotent replace-or-append behavior so no caller can get it wrong.

---------

Co-authored-by: shaun smith <1936278+evalstate@users.noreply.github.com>
2026-07-19 09:52:43 -04:00
Alexander Savchuk
2899ffb6f3
Fix #4292: SSRF guard breaks OAuth/JWKS fetches behind a corporate HTTP proxy (#4412)
* Add FASTMCP_SSRF_TRUST_PROXY to allow SSRF fetches through a corporate proxy

🤖 Generated with Claude Code

* Make SSRF fetch client trust_env explicit for proxy routing

🤖 Generated with Claude Code

* Warn when SSRF proxy trust is enabled without a configured proxy

🤖 Generated with Claude Code

* Warn when NO_PROXY would send an SSRF-trust-proxy fetch direct

🤖 Generated with Claude Code

* Refuse SSRF-trust-proxy fetches when no proxy would route the target

🤖 Generated with Claude Code

* Fix TestProxyMode mocks to patch httpx2.AsyncClient

main's httpx -> httpx2 migration (#4503) landed after these tests were
written; ssrf.py's fetch path already uses httpx2.AsyncClient, but
TestProxyMode still patched the old httpx module, so the mock silently
stopped intercepting and requests escaped to the real network.

* Fix port-qualified NO_PROXY bypass in SSRF proxy-trust guard

proxy_bypass(hostname) discarded the port, so a NO_PROXY entry like
127.0.0.1:8443 went undetected while httpx2 honored it and sent the
request direct with the blocklist already disabled. Pass host:port
instead, except for IPv6 literals, where httpx2 ignores port when
matching NO_PROXY and neither bracketed nor unbracketed host:port
reliably matches through proxy_bypass()'s own parser.

* Replace NO_PROXY prediction with explicit proxy control in SSRF trust-proxy mode

Predicting httpx2's proxy routing (via proxy_bypass(), then via httpx2's own
get_environment_proxies()/URLPattern internals) kept diverging from its real
NO_PROXY handling — three rounds, three different divergences, always in the
unsafe direction. Read HTTPS_PROXY/ALL_PROXY directly and pass it to httpx2
explicitly with trust_env=False, so the request provably goes through that
proxy instead of being predicted to. NO_PROXY is no longer evaluated in this
mode: a NO_PROXY'd host is now routed through the proxy rather than refused,
since that's strictly safer than the alternative (direct with the blocklist
already off).

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:42:52 -04:00
Pierre Audonnet
a57f1c8b20
Add subject field to AccessToken initialization (#4267)
* Add subject field to AccessToken initialization

Fixes #4266

Add `subject` property to the AccessToken.

```python
        return AccessToken(
            token=access_token_as_dict["token"],
            client_id=access_token_as_dict["client_id"],
            scopes=access_token_as_dict["scopes"],
            subject=access_token_as_dict["subject"],
            # Optional fields
            expires_at=access_token_as_dict.get("expires_at"),
            resource=access_token_as_dict.get("resource"),
            claims=access_token_as_dict.get("claims") or {},
        )
```

* Populate AccessToken.subject across all token verifiers

Closes #4266. get_access_token().subject was always None: the SDK's
AccessToken.subject wasn't carried into FastMCP's AccessToken by the
dependency-layer conversion, and none of the built-in TokenVerifiers
(JWT, introspection, and the OAuth-provider verifiers for Discord,
Clerk, Google, WorkOS, HuggingFace, GitHub, and Cognito) populated it
from the sub claim/field they already extract.

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:15:13 -04:00
vijaydeepsinha
654335607c
Fix-issue-4284 : Add Auth0MCPProvider for Auth0 Auth for MCP (#4411)
* Add Auth0MCPProvider for Auth0 Auth for MCP

* Document Auth0 MCP provider integration

* Add Auth0MCPProvider scope and auth rejection tests

Cover permissions-based required_scopes enforcement and unauthenticated MCP 401 responses.

* fixed documentation

* Narrow Auth0 docs to integration guide only

* Fix Auth0 provider: use httpx2 instead of httpx

httpx is a dev-only transitive dependency in this repo; runtime installs
declare httpx2 exclusively. The module-level 'import httpx' in auth0.py
broke import on a clean install of fastmcp or fastmcp-slim[server].

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:15:02 -04:00
Jeremiah Lowin
243f054f65
Include scopes in auth challenges (#4527) 2026-07-18 20:53:52 -04:00
Jeremiah Lowin
998b37f32b
Add server-side identity assertion (SEP-990 ID-JAG) (#4483)
* Add server-side SEP-990 identity assertion (ID-JAG)

* Test SEP-990 identity assertion token endpoint

* Format identity assertion test

* Document SEP-990 identity assertion

* Thread identity_assertion through OIDCProxy

* Harden ID-JAG: authoritative scopes and grant-type enforcement

Scopes for the issued token now derive only from the signed assertion (or server policy when it omits scopes); the client-supplied request scope may narrow but never widen them. Enforce the registered grant-type constraint the SDK check bypassed, and have proxy DCR add the jwt-bearer grant to registered clients when identity assertion is enabled.

* Harden ID-JAG: honor nbf, reject non-object payload, bound jti cache, preserve required_scopes

* Document per-process ID-JAG replay limitation and nbf check

* Harden ID-JAG round 3: resource indicator, non-object header, algorithm config

- Honor RFC 8707 resource on the jwt-bearer grant (invalid_target on
  mismatch), mirroring authorize()'s invariant incl. skip-when-unconfigured
- Reject JSON-array JOSE headers with invalid_grant instead of a 500
- Add IdentityAssertion.algorithm so ES256/PS256 issuers can be verified
  (JWTVerifier otherwise defaults to RS256)

* Bind ID-JAG exchange to the assertion's signed client_id and resource

SEP-990: the IdP signs which client and which resource the assertion was
minted for. With public proxy clients the presented client_id is
self-asserted, so the signed binding is what stops client B redeeming
client A's leaked assertion — and the signed resource claim stops an
assertion for server A being redeemed at server B behind the same IdP.

* Harden ID-JAG round 4: check bindings before jti consumption; validate temporal claims, algorithm, and discovery body

- Move the client_id/resource binding checks into the validator itself,
  before jti is recorded as consumed, so an assertion presented with the
  wrong binding is rejected without burning replay protection for whoever
  it actually belongs to
- Reject non-numeric exp/iat/nbf with invalid_grant instead of a 500
- Validate IdentityAssertion.algorithm at config time (must be an
  asymmetric JWS algorithm verifiable via JWKS)
- Reject a non-object OIDC discovery body with invalid_grant instead of a 500
- Centralize the resource-URL comparison helpers used by both the
  validator and OAuthProxy.authorize()

* Rebase onto httpx2/SDK b2 and harden ID-JAG round 5

- Migrate identity assertion + tests to httpx2 and the local httpx2_mock
  (legacy httpx is now banned; pytest-httpx no longer intercepts)
- Add is_optional to the shared httpx2_mock, mirroring pytest-httpx
- Tighten the algorithm allowlist to JWTVerifier's exact supported set
  (prefix check accepted typos like RS999 -> 500 on first exchange)
- Reject non-string jti before the cache lookup (unhashable -> 500)
- Track revocation for self-contained ID-JAG tokens: revoke_token records
  the jti and load_access_token rejects it until natural expiry
- Dedupe resource-URL helpers: proxy now imports the shared
  normalize_resource_url/server_url_has_query from identity_assertion

* Advertise 'none' token-endpoint auth method when ID-JAG is enabled without CIMD

DCR clients are public, so metadata consumers must see 'none' to use the
advertised jwt-bearer grant; previously only the CIMD path added it.

* Document 2026-07-28 protocol support as a distinct feature catalog

SEP-990 identity assertion leads: the SDK provides the wire contract and
provider hook; FastMCP provides the complete server-side implementation.
Inventories the full modern-era capability set for v4.

* Harden ID-JAG round 6: lazy re-export, dual-form audience, per-issuer algorithms, discovery backoff

- IdentityAssertion re-exported lazily from server.auth (the eager import
  bypassed the package's documented lazy-import boundary)
- Accept the ID-JAG aud both with and without base_url's trailing slash;
  metadata advertises the slashed form, so IdPs echoing it verbatim work
- algorithms={issuer: alg} per-issuer override, mirroring jwks_uris
- OIDC discovery serializes per-issuer and backs off 30s after a failure
  (discovery runs pre-signature, so garbage could amplify into HTTP floods)
2026-07-18 19:52:15 -04:00
Kevin J Gao
3fdeedb567
Improve DescopeProvider scope discovery and well-known URL support (#4489)
* Improve DescopeProvider scope discovery and well-known URL support

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify DescopeProvider scope and URL handling

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make DescopeProvider scope discovery async and lazy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code

* Use generic scope in Descope tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code

* Address Descope discovery edge cases

* Deduplicate Descope metadata fallback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 16:18:45 -04:00
Jeremiah Lowin
18b5ab5852
Migrate to MCP SDK v2.0.0b2 (httpx2) (#4503) 2026-07-18 15:12:47 -04:00
Jeremiah Lowin
66c0270bc1
Stabilize upgraded ty checks (#4526) 2026-07-17 17:46:53 -04:00
Burt Matthews
1fca15abe6
Skip unsupported JWKS keys instead of failing the whole key set (#4515) (#4517) 2026-07-17 17:28:43 -04:00
nate nowack
1d932cc778
Fix Azure scope fallback (#4469) 2026-07-09 15:29:50 -04:00
Jeremiah Lowin
9f251bad00
Forward-port Hugging Face auth provider (#4475) 2026-07-08 20:56:07 -04:00
Jeremiah Lowin
3522a98766
Migrate to MCP Python SDK v2 (#4437) 2026-07-06 17:36:45 -04:00
Jeremiah Lowin
c1b0396c0a
Block IPv6 transition SSRF bypasses (#4426) 2026-07-05 17:52:57 -04:00
Jeremiah Lowin
67527c1f69
Block unsafe OAuth redirect schemes (#4419) 2026-07-05 17:16:10 -04:00
Jeremiah Lowin
cccb529f50
Fix DCR redirect URI validation (#4408) 2026-07-05 17:05:13 -04:00
Jeremiah Lowin
de521e651d
Fix versioned auth middleware checks (#4401) 2026-06-28 10:43:34 -04:00
Jeremiah Lowin
5de15e0c21
Block NAT64 SSRF bypass (#4400) 2026-06-27 12:21:37 -04:00
Jacky Su
0668eb7b80
Fix/cimd flow issue (#4206) 2026-06-27 10:57:58 -04:00
Jeremiah Lowin
a612846224
fix: bound default OIDC discovery timeout and expose it on provider wrappers (#4374) 2026-06-24 17:16:18 -04:00
Bobby Davidson
7f2d034f4d
Forward IdP auth errors to MCP client instead of showing HTML error page (#4293)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-06-24 12:09:09 -04:00
Jeremiah Lowin
3b8538e242
Allow private JWT headers (#4290) 2026-06-06 02:23:48 +01:00
Jeremiah Lowin
e58f386bac
Log refresh-token misses in OAuthProxy instead of failing silently (#4276) 2026-06-03 11:22:35 -04:00
Jeremiah Lowin
58e0f5320b
Decouple FastMCP access token lifetime from upstream expires_in (#4254) 2026-05-30 11:48:23 -04:00
Jeremiah Lowin
53b20168c8
Close upstream OAuth clients (#4248) 2026-05-27 19:45:45 -04:00
Bill Easton
802ceaaa6b
Add targeted coverage tests (#4230)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-25 18:57:28 -04:00
Jeremiah Lowin
b8c9d58d61
Migrate auth JWTs to joserfc (#4221) 2026-05-23 09:29:04 -04:00
rgillinlz
81a16ed27f
fix(auth): unprefix Azure scopes echoed back to MCP clients (#4130)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 12:22:24 -04:00
mohanram
0022d8518f
Add token_expiry_threshold_seconds for proactive token refresh (#4142)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 10:47:26 -04:00
Shigechika AIKAWA
2a262438fa
fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata (#4167)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:36:01 -04:00
Tiago Surjus Kaneta
b0fb2c3ae6
Add valid_scopes and extra_authorize_params to WorkOSProvider (#4135) 2026-05-20 10:31:48 -04:00
Bill Easton
344a4f81b5
fix(auth): disambiguate auth-denied vs missing component messages (#4165) 2026-05-20 09:36:27 -04:00
Bill Easton
11951585c0
fix(tests): hoist azure-identity import out of the OBO test timeout window (#4176) 2026-05-20 09:35:56 -04:00