upstream-token-passthrough

The caller's access token is never passed to the upstream

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 upstream-token-passthrough.

pack
upstream opt-in pack
weight
criticalcandidate
scope
server (what the harness's upstream stub received from the server, and what the server printed or relayed)
decided by
upstream
since
0.3-draft (methodology 0.3-draft)
config keys
checks.upstream-token-passthrough.enabledchecks.upstream-token-passthrough.severityidentities.aidentities.boauth.issuerupstream.credential_headerupstream.kind

Probes

  • identity-a every valid call as identity a, whose token is an access token the harness's issuer minted for this server ({audience}); every header, query and body the upstream stub receives is searched for the token and for its signature
  • identity-b the same calls as identity b

Settings and thresholds

Every check also takes checks.upstream-token-passthrough.enabled and checks.upstream-token-passthrough.severity; they are described once in the configuration reference.

This check has no numeric threshold.

KeyDefaultStandardMeaning
identities.anonenot lockedToken the harness sends as caller A wherever the server config says {bearer} or {bearer:a}. null mints one per run. Set it when the server validates tokens it did not issue. ${VAR} is read from the environment. Treated as a secret: redacted in config show and in reports (flag --identity-a).
identities.bnonenot lockedToken the harness sends as caller B ({bearer:b}). null mints one per run. Must differ from identities.a. Treated as a secret (flag --identity-b).
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.
upstream.credential_headerauthorizationnot lockedHeader the upstream-credential check reads the forwarded credential from (compared case-insensitively).
upstream.kindnonenot lockedKind of the harness-owned upstream stub the server's backend is pointed at through {upstream}. http starts a loopback stub that records every request the server forwards (flag --upstream). null starts none. The stub needs a server Sealmark starts, and the server must use {upstream} in command, args, env, url, headers or files.

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

Never forward the token a client sent you to an upstream API: it was issued for this server (its audience), and passing it on lets the upstream act as the client and hides who called. Use a credential of the server's own for the upstream, or exchange the client's token for one issued to the upstream (a token exchange or on-behalf-of grant), and keep the original token out of headers, query strings and bodies you send onwards.

False positives

None known: the token is minted for this run and audience-bound to the server under test, and the MCP authorization specification forbids passing it to an upstream API (the server must obtain its own token for the upstream).

False negatives

Only the harness's valid calls are observed; a tool the harness did not call can still forward the token. A token transformed beyond recognition (re-encoded, split) is not seen; its signature segment alone is searched for as well.