Skip to content

withMcpAuth returns 403 instead of 401 for expired tokens that also lack scopes #181

Description

@anshusaurav

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_token tells the client its token is no longer valid → the client should refresh or re-authenticate
  • 403 insufficient_scope tells 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";consthandler=withMcpAuth(()=>newResponse("ok"),(_req,bearer)=>({token: bearer!,clientId: "c1",scopes: ["read"],// ← missing "admin"expiresAt: Math.floor(Date.now()/1000)-60,// ← expired}),{required: true,requiredScopes: ["admin"]},);constres=awaithandler(newRequest("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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions