oauth-discovery
A client without a token is told where to authorize (RFC 9728)
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-discovery.
- 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-discovery.enabledchecks.oauth-discovery.severityoauth.audienceoauth.issuerserver.headersserver.url
Probes
no-token-initializeinitialize without a token: HTTP 401 with WWW-Authenticate: Bearer resource_metadata="<url>"metadataGET the resource_metadata URL (same origin only) and the RFC 9728 well-known paths (path-suffixed and root): JSON whose resource is the server URL (no trailing slash) and whose authorization_servers is not empty (with --oauth: names {issuer})tools/list-no-tokentools/list without a token: HTTP 401tools/list-garbage-tokentools/list with a garbage bearer token: HTTP 401tools/call-no-tokentools/call without a token: HTTP 401tools/call-garbage-tokentools/call with a garbage bearer token: HTTP 401resources/list-no-tokenresources/list without a token: HTTP 401resources/list-garbage-tokenresources/list with a garbage bearer token: HTTP 401prompts/list-no-tokenprompts/list without a token: HTTP 401prompts/list-garbage-tokenprompts/list with a garbage bearer token: HTTP 401
Settings and thresholds
Every check also takes checks.oauth-discovery.enabled and checks.oauth-discovery.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. |
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
Answer a request without a valid token with 401 and WWW-Authenticate: Bearer resource_metadata="<url>", and serve that RFC 9728 document (also at /.well-known/oauth-protected-resource<path>): JSON with resource set to this server's URL exactly (https, no trailing slash) and authorization_servers listing the issuer clients should use. Check the token before anything else, on every method (tools/list, tools/call, resources/list, prompts/list), with or without a session.
False positives
A resource_metadata URL on another origin is not fetched (the harness contacts only the server's origin); the well-known paths are tried instead. A deployment that publishes its metadata only on another origin fails.
False negatives
The metadata is checked for resource and authorization_servers only; the authorization server's own metadata is not fetched. n/a when the server answers requests without a token (http-auth grades that).