oauth-discovery

A client without a token is told where to authorize (RFC 9728)

shipped

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
oauth opt-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-initialize initialize without a token: HTTP 401 with WWW-Authenticate: Bearer resource_metadata="<url>"
  • metadata GET 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-token tools/list without a token: HTTP 401
  • tools/list-garbage-token tools/list with a garbage bearer token: HTTP 401
  • tools/call-no-token tools/call without a token: HTTP 401
  • tools/call-garbage-token tools/call with a garbage bearer token: HTTP 401
  • resources/list-no-token resources/list without a token: HTTP 401
  • resources/list-garbage-token resources/list with a garbage bearer token: HTTP 401
  • prompts/list-no-token prompts/list without a token: HTTP 401
  • prompts/list-garbage-token prompts/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.

KeyDefaultStandardMeaning
oauth.audiencenonenot lockedThe 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.issuernonenot lockedWhich 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.headersnonenot lockedHTTP headers sent to the server (url transport). Values under secret-looking names are redacted.
server.urlnonenot lockedURL 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).