Skip to content

bug(auth): tool-level authRequired silently 401s every call when authService has mcpEnabled: true #3243

@cbcoutinho

Description

@cbcoutinho

Toolbox version

us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:1.2.0 (also reproduces on main as of today)

Summary

Combining mcpEnabled: true on an authService with tool-level authRequired referencing that same auth service makes every authenticated tool invocation fail with HTTP 401 unauthorized Tool call: Please make sure you specify correct auth headers — even when the Bearer token is valid and the server-level MCP auth check at internal/server/server.go:514 passed.

Root cause

The MCP method router does a second, per-tool authorization pass that requires some auth service to have produced claims via GetClaimsFromHeader:

internal/server/mcp/v20251125/method.go:176-198:

for _, aS := range authServices {
    claims, err := aS.GetClaimsFromHeader(ctx, header)
    if err != nil { logger.DebugContext(ctx, err.Error()); continue }
    if claims == nil { continue }
    claimsFromAuth[aS.GetName()] = claims
}

// Tool authorization check
verifiedAuthServices := make([]string, len(claimsFromAuth))
...
isAuthorized := tool.Authorized(verifiedAuthServices)
if !isAuthorized {
    err = util.NewClientServerError(
        "unauthorized Tool call: Please make sure you specify correct auth headers",
        http.StatusUnauthorized, nil)
    ...
}

But the generic auth service's GetClaimsFromHeader short-circuits to nil, nil whenever mcpEnabled is true (internal/auth/generic/generic.go:186-189):

func (a AuthService) GetClaimsFromHeader(ctx context.Context, h http.Header) (map[string]any, error) {
    if a.McpEnabled {
        return nil, nil
    }
    ...
}

So claimsFromAuth is always empty when MCP Auth is enabled, verifiedAuthServices is empty, and any tool with authRequired: [<name>] referencing that auth service fails tool.Authorized([]).

Reproduction

Server config:

kind: source
name: bq
type: bigquery
project: my-project
---
kind: authService
name: my-google-auth
type: generic
audience: <client_id>.apps.googleusercontent.com
authorizationServer: https://accounts.google.com
introspectionEndpoint: https://www.googleapis.com/oauth2/v3/tokeninfo
introspectionMethod: GET
introspectionParamName: access_token
mcpEnabled: true
scopesRequired: [openid, email]
---
kind: tool
name: query_thing
type: bigquery-execute-sql
source: bq
description: ...
authRequired:
  - my-google-auth   # ← this is the trap
  1. Complete OAuth via any MCP client (claude.ai, Inspector).
  2. Call query_thing with a valid Bearer access token.
  3. Server-level ValidateMCPAuth passes (introspection validates aud, scope, exp).
  4. Method router returns 401 unauthorized Tool call: Please make sure you specify correct auth headers.
  5. Logs show no clue about why — no audience/scope/issuer error, just the generic message.

Remove authRequired from the tool and the same setup works. So the combination is the bug, not either field alone.

Why this is a footgun

  • The error message blames the client ("specify correct auth headers"), but the client did everything right.
  • The fix (removing authRequired) makes the security posture look weaker to a casual reader, even though MCP Auth already gates the entire server, so tool-level authRequired was always redundant.
  • There's no startup-time check or warning when both are set together — the config loads cleanly and only the runtime behavior reveals the issue.

Suggested fixes (any one)

  1. Treat MCP-authenticated requests as verified at the tool layer. When mcpEnabled is set, populate claimsFromAuth[authServiceName] from the context that ValidateMCPAuth already stashed via util.WithAuthTokenClaims, instead of calling GetClaimsFromHeader again.
  2. Skip tool-level authRequired enforcement when MCP Auth is enabled. The server-level check is the authoritative gate in that mode; the tool list of acceptable auth services is meaningless when there's only one (MCP-gated) entry point.
  3. Reject the combination at config-load time with a clear error: "tool 'X' has authRequired: [Y], but authService 'Y' has mcpEnabled: true — these are mutually exclusive. Remove authRequired from the tool; MCP Auth gates the entire server."

Option 3 is the cheapest (config validation only) and prevents footgun behavior without changing runtime semantics. Option 1 or 2 actually makes the combination usable.

Related

The Google IdP path also currently hits #3240 (introspection parser can't read Google's string-typed numeric fields). Both bugs need fixing for the documented "Google as OIDC provider with MCP Auth" config to work end-to-end. Both reproduce against :1.2.0 and main.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions