Skip to content

Commit f52ea6b

Browse files
author
Max Loffgren
committed
fix(auth): tag unparseable introspection bodies
An introspection body that failed to parse, or that parsed to null, threw past every tagged error in introspectToken. It reached the client as a 401 with WWW-Authenticate and the raw parser message in error_description, so an upstream fault pushed a working session into reauthorization and no reason was logged. Parse defensively and treat a missing boolean `active` as introspect_malformed_body, matching the guard this file already uses on other upstream JSON reads. Also scope the telemetry doc comment: it covers rejections raised while authenticating, not the outbound client tags raised during tool execution.
1 parent d7541ab commit f52ea6b

2 files changed

Lines changed: 25 additions & 2 deletions

File tree

src/index.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,14 @@ async function introspectToken(
473473
status: response.status,
474474
});
475475
}
476-
const data = (await response.json()) as OAuthIntrospectionResponse;
477-
if (typeof data.active !== 'boolean') {
476+
// A body that fails to parse, or that parses to something without a boolean
477+
// `active`, is an unusable answer rather than a verdict on the credential.
478+
// Reading `active` off `null` would throw past every tagged error here and
479+
// reach the client as an OAuth challenge carrying raw parser text.
480+
const data = (await response.json().catch(() => null)) as
481+
| OAuthIntrospectionResponse
482+
| null;
483+
if (!data || typeof data.active !== 'boolean') {
478484
throw new CredentialValidationUnavailableError({
479485
elapsedMs: elapsedMs(),
480486
reason: 'introspect_malformed_body',
@@ -748,6 +754,10 @@ function emitLegacyKeyPathTelemetry(
748754
* introspection outage, a missing secret, and a credential that introspected
749755
* cleanly but cannot be used here are indistinguishable after the fact.
750756
*
757+
* Scope is rejections raised while authenticating a request. The outbound client
758+
* tags are raised during tool execution and surface as tool errors, so they do
759+
* not reach this hook.
760+
*
751761
* Intentionally low cardinality. Never add the token, the resolved API key, the
752762
* upstream response body, request URLs, user agents, or hashes of any of them.
753763
*/

tests/mcp-smoke.test.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,19 @@ test('each credential validation failure logs its own reason and status', async
20372037
reason: 'introspect_malformed_body',
20382038
respond: () => ({ body: JSON.stringify({ active: 'yes' }), status: 200 }),
20392039
},
2040+
{
2041+
// A JSON null would throw on the `active` read and escape as an OAuth
2042+
// challenge carrying the raw parser message.
2043+
expectedStatus: 200,
2044+
reason: 'introspect_malformed_body',
2045+
respond: () => ({ body: 'null', status: 200 }),
2046+
},
2047+
{
2048+
// Truncated body under a JSON content type: same escape route.
2049+
expectedStatus: 200,
2050+
reason: 'introspect_malformed_body',
2051+
respond: () => ({ body: '{"active":true', status: 200 }),
2052+
},
20402053
{
20412054
// Introspection answered cleanly and the credential it described cannot
20422055
// be used on this resource. Tagged apart from an outage.

0 commit comments

Comments
 (0)