oauth-scope
Scopes are enforced and tokens are read from the header only
The sections below are generated from the CLI (methodology 0.3-draft, CLI 0.3.0); the same text is printed by sealmark explain oauth-scope.
- pack
oauthopt-in pack- weight
- scoredcandidate
- scope
- server as deployed (the bearer tokens the endpoint accepts, and what it tells a client that has none)
- decided by
- http
- since
- 0.3-draft (methodology 0.3-draft)
- config keys
checks.oauth-scope.enabledchecks.oauth-scope.severityoauth.audienceoauth.issueroauth.scopesserver.headersserver.url
Probes
controla well-formed RS256 token from the harness's issuer, unexpired, with the right iss, aud and scope: initialize and tools/list must succeed, or the check is inconclusiveno-scopeno scope claim (only when oauth.scopes is declared): refused, 403 insufficient_scope preferred, 401 acceptedunrelated-scopescope "sm-unrelated" (only when oauth.scopes is declared): refused, 403 insufficient_scope preferred, 401 acceptedquery-tokenthe control token as ?access_token= in the URL and no Authorization header: never served (RFC 6750 2.3; the MCP specification requires the header)
Settings and thresholds
Every check also takes checks.oauth-scope.enabled and checks.oauth-scope.severity; they are described once in the configuration reference.
This check has no numeric threshold.
| Key | Default | Standard | Meaning |
|---|---|---|---|
oauth.audience | none | not locked | The audience (RFC 8707 resource) the stub's tokens carry and the protected-resource metadata must name; the value of {audience}. null uses server.url in canonical form (no fragment, no trailing slash). |
oauth.issuer | none | not locked | Which OAuth issuer the server under test trusts. stub starts the harness's own authorization server on loopback (flag --oauth): {issuer} and {jwks} name it, and the caller identities ({bearer}, {bearer:a}, {bearer:b}) become tokens it signed for {audience}, sent as Authorization: Bearer. Point the server's issuer, JWKS URL and audience at those placeholders in server.env or server.args. external declares a server that trusts its own issuer: only the checks that need no harness-signed token run (oauth-discovery). null declares nothing. |
oauth.scopes | [] | not locked | Scopes the server requires: the control token carries them, and oauth-scope sends a token without them, which must be refused. Empty skips the scope probe. |
server.headers | none | not locked | HTTP headers sent to the server (url transport). Values under secret-looking names are redacted. |
server.url | none | not locked | URL of a Streamable HTTP server. Alone it must already be running; with command, Sealmark starts it and waits for this URL. |
How the verdict is established
- Scoring. scored: pass = 1 point, part = 0.5, fail = 0. A fail lowers the pack's percentage but never caps the letter.
What to fix
Read the token from the Authorization header only, never from an access_token query parameter (URLs end up in logs). Refuse a token that lacks a scope the request needs, preferably with 403 and WWW-Authenticate: Bearer error="insufficient_scope", scope="<needed>". The scopes the check expects are oauth.scopes.
False positives
None known: the scope probes run only for scopes the configuration says the server requires.
False negatives
Scopes are checked on initialize and tools/list only; a server that enforces scopes per tool is not probed tool by tool. Without oauth.scopes only the query-token leg runs.