fix(wristband): drop OIDC identity scopes at MCP authorize
Clients that also read the issuer's OpenID Connect discovery document
add openid, profile or email to their MCP authorize request, and the
MCP flavour refused those requests with invalid_scope ("scope contains
an unsupported value"), so they could not connect.
The three identity scopes are now dropped before the allow-list check,
as RFC 6749 section 3.3 lets a server grant fewer scopes than requested.
A request left with no other scope gets the read default. Any other
unknown scope, including a differently cased identity name, is still
refused. The MCP flavour still never grants identity scopes or issues
an id_token, and the metadata document is unchanged.
This commit is contained in:
@@ -39,7 +39,7 @@ func newServer() *wristband.Server {
|
||||
> [!WARNING]
|
||||
> Always set `wristband.Options.Resource` for your deployment. `wristband.DefaultOptions` leaves it empty, and while it is empty every authorization request that sends a `resource` parameter is refused with `invalid_target`.
|
||||
|
||||
The defaults cover the rest: pending requests and codes live 10 minutes, access tokens 1 hour and refresh tokens 30 days, at most 200 unrevoked clients may register, and registration bodies are capped at 64 KiB. `wristband.Options.ScopesSupported` is both the advertised list and the authorize allow-list, one resolved slice. A non-empty list is used as given: the example above drops `ai`, so a request for it is `invalid_scope`, and `wristband.DefaultOptions` (`read`, `write`, `ai`, `offline_access`) still refuses `backend`. An empty or nil list on the MCP flavour resolves to `read`, `write`, `ai`, `offline_access` and `backend`. OpenID Connect discovery stays `openid`, `profile` and `email`.
|
||||
The defaults cover the rest: pending requests and codes live 10 minutes, access tokens 1 hour and refresh tokens 30 days, at most 200 unrevoked clients may register, and registration bodies are capped at 64 KiB. `wristband.Options.ScopesSupported` is both the advertised list and the authorize allow-list, one resolved slice. A non-empty list is used as given: the example above drops `ai`, so a request for it is `invalid_scope`, and `wristband.DefaultOptions` (`read`, `write`, `ai`, `offline_access`) still refuses `backend`. The OpenID Connect identity scopes `openid`, `profile` and `email` are dropped from the request before the allow-list check (RFC 6749 section 3.3 lets a server grant fewer scopes than requested), so a client that also reads the issuer's OpenID Connect discovery document still connects; a request with nothing else left gets the default `read`, and the three are never advertised. An empty or nil list on the MCP flavour resolves to `read`, `write`, `ai`, `offline_access` and `backend`. OpenID Connect discovery stays `openid`, `profile` and `email`.
|
||||
|
||||
The MCP advertised set is that resolved list. The fixed names `read`, `write`, `ai`, `offline_access` and `backend` are always on it. Declared member scopes (`pact.FrontendTokenScope`, including `pact.FrontendTokenScope.Implies`) come from the published `pact.MemberTokenScopeCatalog` and are filtered by the OAuth server plugin's key `golem15.oauthserver.mcp_scopes`. That key is the plugin's, not a framework config key: unset means every declared member scope, a set filter means only those declared names, and an empty filter means the five fixed names only. The plugin publishes the list as `pact.MCPAdvertisedScopes`. pier copies `pact.MCPAdvertisedScopes.MCPScopesSupported` when it is non-empty and otherwise keeps `pier.PRMScopes`. The read/write ceiling is unchanged. A route the endpoint runs reads the caller with `pact.MCPCallFrom`: `pact.MCPSubject.TokenID` is a member personal-token id only when `pact.MCPSubject.ClientID` is empty, and the OAuth access-token row otherwise, and `pact.MCPSubject.Scopes` is the grant, not an implied expansion. The metadata document serves the configured values:
|
||||
|
||||
@@ -214,7 +214,7 @@ Before either render method runs, the engine sets `X-Frame-Options: DENY`, `Cont
|
||||
|
||||
### Client kinds
|
||||
|
||||
`wristband.ClientRecord.Kind` and `wristband.OIDCClientRecord.Kind` tell the two kinds of client apart: `wristband.ClientKindMCP` for MCP clients (dynamically registered or issued by an operator command; an empty kind means the same) and `wristband.ClientKindOIDC` for clients an administrator registers. Each server accepts only its own kind at authorize, token and userinfo, and the MCP server keeps refusing the `openid` scope, so a dynamically registered client can never obtain identity scopes or an `id_token`.
|
||||
`wristband.ClientRecord.Kind` and `wristband.OIDCClientRecord.Kind` tell the two kinds of client apart: `wristband.ClientKindMCP` for MCP clients (dynamically registered or issued by an operator command; an empty kind means the same) and `wristband.ClientKindOIDC` for clients an administrator registers. Each server accepts only its own kind at authorize, token and userinfo, and the MCP server drops `openid`, `profile` and `email` at authorize and never issues an `id_token`, so a dynamically registered client can never obtain identity scopes or an `id_token`.
|
||||
|
||||
### Routes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user