Authorization Hardening (MCP 2026-07-28)¶
1. What the spec says¶
The 2026-07-28 release candidate folds six authorization SEPs into the draft
specification under basic/authorization. They do not redesign MCP's OAuth 2.1
profile; they close concrete interoperability and security gaps that have surfaced
since the 2025-11-25 release, primarily where MCP clients connect to OpenID
Connect-flavoured authorization servers and where a single client speaks to many
authorization servers.
SEP-2468 — Authorization-server issuer (iss) validation per RFC 9207¶
The draft now requires authorization servers to include the iss query parameter
on every authorization response (success or error), as specified in RFC 9207
section 2. Servers advertise support via the
authorization_response_iss_parameter_supported: true metadata flag (RFC 9207
section 3). Clients MUST extract iss from the redirect URL, compare it as a
simple string (RFC 3986 §6.2.1) against the issuer they recorded before
redirecting, and abort the flow on mismatch.
The SEP intentionally diverges from RFC 9207 §2.4's "SHOULD discard if not
advertised" recommendation: it specifies "compare if present, reject on
mismatch" so that authorization servers that begin emitting iss before
publishing the metadata flag do not break legitimate flows. The SEP notes that
a future revision is expected to upgrade the server-side SHOULD to a MUST and
to require clients to reject responses that omit iss.
The mix-up attack class this mitigates is described in the OAuth 2.0 Security Best Current Practice and in the formal analyses cited by RFC 9207 (arXiv:1508.04324, arXiv:1601.01229). It is relevant to any client that talks to more than one authorization server — exactly muster's deployment shape.
SEP-837 — OIDC application_type during Dynamic Client Registration¶
The draft now references OpenID Connect Dynamic Client Registration 1.0 as a
normative input and adds an "Application Type and Redirect URI Constraints"
subsection under DCR. MCP clients MUST send application_type on DCR requests
to OIDC-aware servers. Native applications (desktops, CLIs, locally hosted
clients on localhost) SHOULD use application_type: "native"; remote
browser-based applications SHOULD use application_type: "web".
Without this hint, OIDC-flavoured authorization servers default to "web" and
then reject the localhost redirect URIs that a desktop or CLI client needs. The
SEP also requires clients to surface a meaningful error on registration
failure and permits retrying with an adjusted application_type or
type-conforming redirect URIs. Non-OIDC servers ignore the parameter, so the
field is safe to send unconditionally.
SEP-2352 — Authorization server binding and migration¶
Two new clauses are added to the draft:
- A clarification under Protected Resource Metadata that each entry in
authorization_serversis an independent OAuth 2.0 authorization server (RFC 6749 §2.2), and that clients MUST maintain separate registration state per authorization server and MUST NOT assume credentials valid for one are accepted by another. - A new "Authorization Server Binding" subsection that distinguishes the two credential models:
- DCR-issued or pre-registered credentials MUST be keyed by the issuer
they were registered with. When protected resource metadata starts
pointing to a different authorization server, clients MUST NOT reuse the
old credentials and MUST re-register; they SHOULD surface an error rather
than silently retrying with a mismatched
client_id. - Client ID Metadata Documents (CIMD) are explicitly called out as portable across authorization servers, because the document is a self-hosted HTTPS URL that the authorization server resolves on demand. No re-registration is needed when the authorization server changes.
SEP-2207 — OIDC-flavoured refresh token guidance¶
OAuth 2.1 has no standard way for a client to request a refresh token; OIDC
introduced the offline_access scope for this purpose. SEP-2207 standardises
the cross-cutting guidance:
- MCP clients that intend to use refresh tokens SHOULD advertise
refresh_tokenin theirgrant_typesclient metadata, MUST keep refresh tokens confidential in transit and at rest per OAuth 2.1 §4.3, and MAY addoffline_accessto thescopeparameter of the authorization and token requests when the authorization server advertisesoffline_accessinscopes_supported. - Clients MUST NOT assume a refresh token will be issued: the authorization server retains discretion.
- MCP servers (protected resources) SHOULD NOT include
offline_accessinWWW-Authenticatescopeor in Protected Resource Metadatascopes_supported, becauseoffline_accessis a client/AS concern, not a resource requirement (OAuth 2.1 §5.3.1 framesscopeas "the required scope of the access token for accessing the requested resource").
SEP-2350 — Client-side scope accumulation on step-up¶
The draft changes the framing of "Runtime Insufficient Scope Errors" and "Step-Up Authorization Flow" to align with RFC 6750 §3.1:
- Servers SHOULD return the scopes required for the current operation in
the
WWW-Authenticatescopeparameter, not the union of previously granted scopes. Servers SHOULD emit all scopes required for an operation in a single challenge (no per-call incremental drips). Minimum / recommended / extended approaches are described as a server-side UX trade-off only. - Clients are now responsible for scope accumulation: when re-authorising
after a 403 with
insufficient_scope, the client MUST compute the union of its previously requested scope set and the scopes from the current challenge, then request that union, so that previously granted permissions are preserved. - A new note clarifies hierarchical scopes: a client need not deduplicate hierarchically (the AS normalises during token issuance), and servers MUST account for hierarchy when deciding whether a token is sufficient.
SEP-2351 — Default .well-known discovery suffix¶
The draft is updated to state explicitly that MCP uses the default
oauth-authorization-server well-known URI suffix defined in RFC 8414 §3.1,
and that MCP does not define an application-specific suffix. The
multi-endpoint discovery sequence (RFC 8414 path insertion, OIDC path
insertion, OIDC path append, plus the root-path forms) is otherwise unchanged.
This is a pure clarification — required by RFC 8414's rule that any
application using its mechanism must declare its suffix.
Net effect¶
Together these SEPs tighten the MCP authorization profile in three directions:
- Mix-up defence moves from "SHOULD" to "MUST" for clients via
issvalidation. - OIDC interoperability is closed for two recurring failure modes — DCR redirect-URI rejection (SEP-837) and absent refresh tokens (SEP-2207).
- Multi-AS hygiene is made explicit: credentials and tokens are bound to
an
issuer, scope sets are accumulated on the client side, and resource servers stop conflatingoffline_accesswith resource scopes.
2. Linked SEPs and PRs¶
- SEP-2468 "Recommend Issuer (iss) Parameter in MCP Auth Responses": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468
- SEP-837 "Update authorization spec to clarify client type requirements": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837
- SEP-2352 "Clarify authorization server binding and migration": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352
- SEP-2207 "OIDC-flavored refresh token guidance": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2207
- SEP-2350 "Clarify client-side scope accumulation in step-up authorization": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350
- SEP-2351 "Explicitly specify RFC 8414 well-known URI suffix for MCP": https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2351
3. muster impact¶
muster has two distinct OAuth surfaces that this section needs to keep separate:
- The agent-side client (
internal/agent/oauth/) — a CLI/desktop OAuth 2.1 client that authenticates the user against muster's own OAuth server (typically Dex behind muster). It runs a local callback listener on127.0.0.1. - The server-side proxy (
internal/oauth/andpkg/oauth/) — muster's Aggregator acting as an OAuth client to remote, OAuth-protected MCP servers, completing the authorization code flow on behalf of a logged-in user and storing the resulting tokens. The callback handler is HTTP-based (/oauth/callback) and runs on the publicly reachable muster server.
The CIMD model used by muster (the client_id is a self-hosted CIMD URL —
see internal/oauth/client.go lines
196-210 and Manager.NewManager in
internal/oauth/manager.go) is exactly
the case SEP-2352 calls "portable across authorization servers". That
simplifies large parts of SEP-2352 for the server-side proxy, but the
agent-side flow against muster's own AS (and any DCR fallback added later)
still needs to follow the rules.
SEP-2468 — iss validation¶
- Agent-side: already implemented. The agent's local callback already
captures the
issquery parameter (internal/agent/oauth/callback_server.go line 164,CallbackResult.Issdocumented at lines 34-37) and internal/agent/oauth/client.go lines 299-318 implement the "compare if present, reject on mismatch" policy that SEP-2468 specifies, including the explicit note that emptyissis treated as "not advertised", not as a mismatch. - Server-side: implemented. The proxy callback at
internal/oauth/handler.go
HandleCallbackreadsissand hands it tovalidateResponseIssuerbefore it acts on anything else in the response, so a response that cannot be attributed to the expected authorization server is rejected whole, error parameters included. The expected value is the issuer from the server's own metadata, and the comparison is a simple string comparison with no normalization. When the metadata is unreachable the comparison falls back toOAuthState.Issuerand then ignores a trailing slash on either side, because that value is operator-configured rather than published by the server. The metadata is trustworthy for the first comparison becausepkg/oauth.Client.DiscoverMetadataapplies the RFC 8414 §3.3 identity check and refuses a document whoseissuernames a different server. - Metadata side: implemented.
pkg/oauth/types.goMetadatacarriesauthorization_response_iss_parameter_supported, and both the proxy and the agent refuse a response that omitsisswhen the server advertises the flag. - Coverage caveat. An authorization server that neither sends
issnor advertises the flag is accepted without a comparison, which is most deployed servers today, dex included. The check therefore adds nothing against them. What does protect those flows is unrelated toiss: the issuer is recorded with the flow state, and the code is redeemed only at the token endpoint of that issuer's own metadata, so a response redirected from a different authorization server cannot be exchanged.
SEP-837 — application_type in DCR¶
- muster does not currently implement Dynamic Client Registration:
client_idon the server side is a CIMD URL (internal/oauth/client.go lines 18-31,GetClientMetadatalines 196-210), and on the agent side the agent usesmcp-oauthproviders (internal/agent/oauth/client.golines 12-17).pkg/oauth/types.goClientMetadata(lines 262-278) has noapplication_typefield. - Impact today is therefore latent, not active: muster does not call
registration_endpoint. The impact is on the CIMD content muster publishes and on any future DCR fallback. The CIMD served byHandler.ServeCIMD(internal/oauth/handler.go lines 197-220) — built fromClient.GetClientMetadata— has no client-type hint. Authorization servers that resolve a CIMD URL and synthesise a registration may apply the same"web"default the SEP warns about. - The agent path matters because the agent runs against
127.0.0.1redirects (internal/agent/oauth/callback_server.go line 81). If muster's AS is OIDC and ever enables DCR, the agent must sendapplication_type: "native". The CLI binary (cmd/auth_login.go,cmd/auth_helpers.go) is the seam where that intent is known.
SEP-2352 — Issuer binding and re-registration¶
- Token store: already correct. Tokens are keyed by
TokenKey{SessionID, Issuer, Scope}(internal/oauth/types.go lines 7-15) andOAuthStatepersistsIssuerfor the duration of the flow (lines 21-46), so a successful callback can only write into the issuer it started with. - Client credential binding: not directly applicable to muster today.
The server-side proxy uses one CIMD
client_idfor all upstream authorization servers, which SEP-2352 explicitly designates as portable — no re-registration is required when an AS changes. - Multi-AS clarification matters at config time: muster's per-server
AuthServerConfig(internal/oauth/manager.go lines 40-45) already keys server configuration byIssuer. TreatingIssuerchanges for an already-registered MCP server as an "AS migration" (drop and re-establish tokens, surface a clear error) is the SEP-2352-aligned behaviour. TodayRegisterServeroverwrites the entry silently (manager.golines 179-196); cached tokens for the old issuer would simply stop being looked up because theTokenKey.Issuerchanges, but no explicit error is surfaced. - Pre-registered DCR credentials (if muster ever introduces them as a
fallback to CIMD) MUST be persisted alongside the
issuerthey belong to, with a check on read.
SEP-2207 — Refresh tokens with OIDC¶
- Agent: already aligned.
agentOAuthScopesin internal/agent/oauth/client.go line 25 already includesoffline_access, so the agent advertises its intent to refresh. Tokens persisted in pkg/oauth/types.goToken(lines 67-91) preserveRefreshTokenandExpiresAt. - Server-side CIMD: already advertises
refresh_token.Client.GetClientMetadatareturnsGrantTypes: ["authorization_code", "refresh_token"](internal/oauth/client.go lines 196-210). That satisfies the "advertise capability" half of SEP-2207. - Server-side scope handling: partial gap. The per-server
Scopeconfigured inAuthServerConfigis fed directly intoClient.GenerateAuthURL(internal/oauth/client.go lines 108-148). There is no logic to conditionally appendoffline_accesswhen the discovered authorization server metadata listsoffline_accessinscopes_supported. SEP-2207 frames this as "MAY" for clients that want a refresh token; muster does want one (sessions outlive a single 401 retry), so this is worth adding for the server-side proxy path as well, gated on metadata. -
Resource-server side: muster acts as a protected resource via its own MCP API (
internal/aggregator/auth_resource.go,internal/aggregator/auth_tools.go). AnyWWW-Authenticateor Protected Resource Metadata muster ever returns MUST NOT includeoffline_access. Today muster does not synthesise these payloads in code paths I grepped (theWWW-Authenticateheaders parsed bypkg/oauth/www_authenticate.goare inbound, from upstream MCP servers); this is therefore a guardrail to keep in mind when the inbound-auth surface grows. -
Refresh carries the resource indicator, from mcp-go. mcp-go owns token refresh for both the agent and the backends, and its
OAuthHandler.refreshTokenputs the RFC 8707resourceon the refresh request. It takes the value from the protected resource metadata it discovers itself, exactly as declared. muster must therefore send the declared value unchanged on the authorization and token requests as well: a value normalized on one path and verbatim on the other binds the initial token and the refreshed token to different audiences. The same constraint applies when the metadata omits the field and both sides derive a value:pkg/oauth.DeriveResourceURIdrops the query and the fragment and changes nothing else, which is what mcp-go does to the same URL (client/transport/streamable_http.go). -
The declared value is only as trustworthy as its source. RFC 9728 §3.2 requires a protected resource to serve its own metadata, so a document is bound to a backend either by the origin-constructed well-known path or by nothing at all. muster therefore follows a
resource_metadata=pointer from aWWW-Authenticateheader only when it is on the backend's own scheme and host (pkg/oauth.ValidateAdvertisedMetadataURL), and accepts aresourcefrom such a document only when it is on that same origin (pkg/oauth.ValidateAdvertisedResource). mcp-go applies the same two checks (client/transport/oauth.govalidateAdvertisedPRMURL, and theexplicitMetadataURLbranch of its discovery). muster compares the origin rather than the whole URI on the second check, because a backend legitimately declares an identifier whose path differs from its MCP endpoint: an mcp-oauth backend serving at<base>/mcpdeclares<base>. A rejected pointer falls through to the well-known path; a rejected resource is dropped and the indicator is derived from the backend URL.
SEP-2350 — Scope accumulation on step-up¶
- Parser: existing.
pkg/oauth/www_authenticate.golines 26-71 parsesscope=out of theWWW-Authenticateheader intoAuthChallenge.Scope(pkg/oauth/types.go lines 191-216). Note thatauthParamRegexonly matcheskey="value"; unquoted scope values (technically allowed by RFC 6750 §3) are not picked up — orthogonal but worth a follow-up. - Accumulation: missing on the server-side proxy.
Client.GenerateAuthURLuses whateverscopestring the caller hands it; there is no logic that says "take the union of the scope previously granted to this(SessionID, Issuer)and the scope in the new 403 challenge". The token store already has the previous scope (it is part ofTokenKey), so the data is available. - Insufficient-scope retries on the inbound side: muster's aggregator
surface
(internal/aggregator/auth_resource.go,
auth_tools.go) returns authentication status to MCP clients but the upstream-401 retry loop lives where the outbound MCP client meetspkg/oauth. Wherever that retry triggers a re-auth, it needs to feed the union scope set intoGenerateAuthURL.
SEP-2351 — .well-known discovery suffix¶
- Already aligned.
Client.doDiscoverMetadatain pkg/oauth/client.go lines 120-165 tries/.well-known/oauth-authorization-serverfirst and falls back to/.well-known/openid-configuration, using the path-insertion and path-append forms RFC 8414 §3.1 and §5 (and the existing 2025-11-25 MCP spec) require. Constants are in pkg/oauth/types.go lines 41-51. - The SEP is a clarification, not a behavioural change for muster.
4. Required changes / migration notes¶
Concrete work items, grouped by SEP and ordered by risk:
- SEP-837 —
application_typeplumbed through CIMD and (future) DCR. - Add an optional
ApplicationType stringfield to pkg/oauth/types.goClientMetadata. - The server-side proxy's CIMD is published at the public muster URL
(internal/oauth/client.go lines
196-210,
Handler.ServeCIMDinternal/oauth/handler.go lines 197-220) — setapplication_type: "web"there. -
The agent CLI runs against
localhostcallbacks — when (or if) it ever performs DCR, it MUST sendapplication_type: "native".cmd/auth_login.goandcmd/auth_helpers.goare the right seam to thread that intent throughinternal/agent/oauth/client.go. -
SEP-2207 — opportunistic
offline_accessfor the server-side proxy. - In
internal/oauth/client.go
GenerateAuthURL, afterDiscoverMetadata, check whether the returnedMetadata.ScopesSupportedcontainsoffline_access. If yes, and the caller did not already include it, append it to the scope string. Pure addition; non-OIDC servers are unaffected. - Keep the CIMD's
grant_typesadvertisingrefresh_token(already in place, lines 196-210). -
When muster grows an inbound
WWW-Authenticateor Protected Resource Metadata path on its own API, never includeoffline_accessin either field (internal/aggregator/auth_resource.go/internal/aggregator/auth_tools.go). -
SEP-2350 — scope accumulation in the outbound retry.
- Where muster sees an upstream
insufficient_scopefrom a remote MCP server, look up the existing tokens for that(SessionID, Issuer, *)via internal/oauth/token_store.goTokenStore.GetAllForSessionand union the scopes of any non-expired entry withAuthChallenge.Scope(parsed from the upstreamWWW-Authenticateper pkg/oauth/www_authenticate.go). - Pass that union to
Client.GenerateAuthURL. The previous scope set is already available as a side-product ofTokenKey.Scope; no additional storage is required. -
Drop or normalise duplicates only on whitespace; do not attempt hierarchy collapsing on the client side (the AS handles that — see SEP-2350's note).
-
SEP-2352 — explicit error on AS migration.
- In
Manager.RegisterServer(internal/oauth/manager.go lines 179-196), when an existing entry'sIssuerdoes not match the incomingIssuer, log a warning and calltokenStore.DeleteByIssuer(_, oldIssuer)for already-issued tokens before overwriting. Surface this as a status change on theauth://statusresource (internal/aggregator/auth_resource.go) so users see a clean "re-authentication required" rather than silent token loss. -
Because muster uses CIMD for the
client_id, no DCR re-registration step is needed. Document this explicitly in internal/oauth/doc.go — it is the primary architectural reason the SEP-2352 burden on muster is small. -
SEP-2351 — documentation only.
-
No behavioural change. When updating any user-facing docs that mention the well-known suffix (
docs/contributing/testing/oauth-testing.mdand decision recorddocs/explanation/decisions/005-muster-auth.md), referenceoauth-authorization-serveras the default per RFC 8414 §3.1. -
Tests and scenarios.
internal/oauth/handler_test.goneeds cases for:issmatches,issmismatches,issabsent (legacy AS).internal/server/oauth_http_test.gochecks the externally visible OAuth HTTP shape; add expectations forapplication_typein the CIMD response.- BDD scenarios under
internal/testing/scenarios/oauth-*already cover refresh and multi-issuer flows (e.g.oauth-different-issuers.yaml,oauth-automatic-token-refresh.yaml,oauth-token-refresh-flow.yaml,oauth-sso-shared-issuer.yaml). The new scenarios needed are:issmismatch rejection (server-side),issabsence acceptance, scope accumulation on step-up. These should follow the existinginternal/testing/mock/oauth_server.gomock pattern.
5. Open questions¶
- AS migration UX. When
Manager.RegisterServersees an issuer flip for an existing remote MCP server, should muster invalidate previously stored tokens silently, surface a "reauth required" status onauth://status, or both? SEP-2352 says "surface an error rather than silently attempt to use mismatched credentials" — translating that into muster's per-server UI vocabulary needs a product call. - Scope accumulation across
Scopekeys. Tokens are stored underTokenKey{SessionID, Issuer, Scope}— meaning a step-up that asks for a wider scope produces a new token-store entry rather than refreshing the old one. Is that the right model after SEP-2350, or should we collapse toTokenKey{SessionID, Issuer}and treat scope as metadata on the entry? - Inbound auth surface. muster's own MCP API today returns auth
status via the
auth://statusresource; it does not advertiseWWW-Authenticateor Protected Resource Metadata on its tools. If that changes (per ADR-008 evolution indocs/explanation/decisions/005-muster-auth.md), SEP-2207 / SEP-2350 guardrails on what not to include need to be applied to whatever code path generates those payloads. - CIMD-vs-DCR matrix. ~~SEP-2352 is favourable to muster precisely
because the proxy uses CIMD. The reverse question is whether muster
should ever fall back to DCR for authorization servers that refuse to
resolve a CIMD URL, and if so, where the per-AS DCR state lives. That
is the only path on which SEP-837 (
application_type) becomes a blocking concern instead of a defensive one.~~ Resolved (issue #1083): the server-side proxy now checksclient_id_metadata_document_supportedon the discovered AS metadata and prefers CIMD whenever advertised; otherwise it registers itself via RFC 7591 when aregistration_endpointis advertised (requestingtoken_endpoint_auth_method: none, sendingapplication_type: "web"per SEP-837), storing the issued credentials keyed by issuer per SEP-2352 (internal/oauth/credential_store.go, Valkey-backed when configured). When neither is advertised the CIMD URL is sent as before, and the auth challenge marks the flowcimd-fallbackso front-ends can warn up front. Stored registrations are verified before each new flow (RFC 7592 client read when the AS providedregistration_client_uri, otherwise a client_id/redirect_uri-only authorization request whose RFC 6749 §4.1.2.1 failure shape shows whether the client is still known) and re-created when the AS has forgotten them, so an AS with an in-memory client store does not leave muster with a poisonedclient_idafter a restart (internal/oauth/client_registration.go).
6. References¶
- 2026-07-28 release-candidate announcement, "Authorization Hardening" section: https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
- Draft authorization specification: https://modelcontextprotocol.io/specification/draft/basic/authorization
- SEP-2468 PR (
issvalidation): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468 - SEP-837 PR (
application_typein DCR): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/837 - SEP-2352 PR (authorization server binding and migration): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2352
- SEP-2207 PR (OIDC-flavoured refresh token guidance): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2207
- SEP-2350 PR (client-side scope accumulation on step-up): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350
- SEP-2351 PR (RFC 8414 well-known URI suffix): https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2351
- RFC 9207 "OAuth 2.0 Authorization Server Issuer Identification": https://www.rfc-editor.org/rfc/rfc9207
- RFC 8414 "OAuth 2.0 Authorization Server Metadata": https://datatracker.ietf.org/doc/html/rfc8414
- RFC 9728 "OAuth 2.0 Protected Resource Metadata": https://datatracker.ietf.org/doc/html/rfc9728
- RFC 7591 "OAuth 2.0 Dynamic Client Registration Protocol": https://datatracker.ietf.org/doc/html/rfc7591
- RFC 6750 §3.1 (
scopeattribute onWWW-Authenticate): https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 - RFC 6749 §2.2 (client identifier uniqueness per AS): https://datatracker.ietf.org/doc/html/rfc6749#section-2.2
- OpenID Connect Dynamic Client Registration 1.0: https://openid.net/specs/openid-connect-registration-1_0.html
- OAuth 2.0 Security Best Current Practice (mix-up attack class): https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics