#181
withMcpAuth returns 403 instead of 401 for expired tokens that also lack scopes
Bug
When withMcpAuth is configured with requiredScopes, an expired token that also lacks a required scope receives 403 insufficient_scope instead of 401 invalid_token.
Per RFC 6750 §3.1, an expired token is invalid_token regardless of what other checks fail. The distinction matters because:
401 invalid_tokentells the client its token is no longer valid → the client should refresh or re-authenticate403 insufficient_scopetells the client it has the wrong permissions → the client will not attempt a refresh and may surface a misleading "insufficient permissions" error to the user
Reproduction
import { withMcpAuth } from "mcp-handler";
const handler = withMcpAuth(
() => new Response("ok"),
(_req, bearer) => ({
token: bearer!,
clientId: "c1",
scopes: ["read"], // ← missing "admin"
expiresAt: Math.floor(Date.now() / 1000) - 60, // ← expired
}),
{ required: true, requiredScopes: ["admin"] },
);
const res = await handler(
new Request("https://example.com/mcp", {
headers: { Authorization: "Bearer tok" },
}),
);
console.log(res.status);
// Actual: 403 (insufficient_scope)
// Expected: 401 (invalid_token)Cause
In src/auth/auth-wrapper.ts, the scope check runs before the expiry check. An expired-and-unscoped token hits the scope gate first and never reaches the expiry gate.
Fix
Move the expiry check above the scope check so that expired tokens are always rejected as invalid_token (401) before scopes are evaluated.
Activity
0No activity yet.