oauth-claims

Expired, early, wrong-issuer and wrong-audience tokens are refused

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-claims.

pack
oauth opt-in pack
weight
criticalcandidate
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-claims.enabledchecks.oauth-claims.severityoauth.audienceoauth.issuerserver.headersserver.url

Probes

  • control a 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 inconclusive
  • expired exp one hour in the past (iat two hours)
  • not-yet-valid nbf one hour in the future
  • no-exp no exp claim
  • wrong-issuer iss https://sm-other-issuer.example (signed by the same key)
  • wrong-audience aud "sm-wrong-audience"
  • no-audience no aud claim
  • other-resource aud https://other.example/mcp: a token the same issuer minted for another resource (RFC 8707)

Settings and thresholds

Every check also takes checks.oauth-claims.enabled and checks.oauth-claims.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. critical and scored: pass = 1 point, part = 0.5, fail = 0. A fail caps the pack's grade at D, whatever the other checks scored.

What to fix

After the signature, check the claims on every request: exp is present and in the future, nbf (when present) is not in the future, iss is exactly the issuer you configured, and aud names this server's own resource URI (RFC 8707); refuse a token minted for another resource even by the same issuer. Allow at most a small clock skew (a minute).

False positives

A server that accepts tokens without exp by design (the JWT access-token profile, RFC 9068, makes exp required) fails no-exp.

False negatives

Times are an hour off, so a server with a clock skew allowance of up to an hour passes; nothing within 60 s is probed. Only the claims listed are probed (not jti replay, not typ).