amruthpillai/reactive-resume · error · AuthError

AuthError

AuthError

Error message

Unauthorized

What it means

Thrown at the end of authenticateRequest after both credential paths have been tried and neither produced a valid principal. The function first attempts a Bearer OAuth token (authorization header) via verifyOAuthToken, then an x-api-key header via auth.api.verifyApiKey; if both are absent or invalid it throws AuthError (message 'Unauthorized'). It is the single gate for MCP HTTP requests, so any MCP client without a valid token or key is rejected.

Source

Thrown at apps/server/src/mcp/auth.ts:32

			const payload = await verifyOAuthToken(authHeader.slice(7));
			if (payload?.sub) return;
		} catch {
			// Invalid or expired token; fall through to API key auth.
		}
	}

	const apiKey = request.headers.get("x-api-key");

	if (apiKey) {
		try {
			const result = await auth.api.verifyApiKey({ body: { key: apiKey } });
			if (result.valid) return;
		} catch {
			// Invalid or malformed key; fall through to AuthError.
		}
	}

	throw new AuthError();
}

View on GitHub (pinned to 3a5b12e2a4)

Solutions

  1. Provide a Better Auth API key: send header 'x-api-key: <key>' generated from the app's API-key endpoint.
  2. Or provide a valid session: send 'Authorization: Bearer <better-auth-session-token>' obtained by logging in through the web app.
  3. Verify the header name is exactly 'x-api-key' (case-insensitive for header lookup, but spelling must match) and that the key is not wrapped in quotes or prefixed with 'Bearer '.
  4. If using a Bearer token, confirm AUTH_SECRET on the server matches the secret that minted the token (mismatch silently invalidates the JWT).

Example fix

// before
await fetch(mcpUrl, { headers: { 'api-key': apiKey } });

// after
await fetch(mcpUrl, { headers: { 'x-api-key': apiKey } });
Defensive patterns

Strategy: validation

Validate before calling

// Validate the request will authenticate before sending.
function buildAuthHeaders(apiKey?: string, bearerToken?: string): Record<string, string> {
  if (bearerToken && bearerToken.startsWith('Bearer ')) return { authorization: bearerToken };
  if (bearerToken && bearerToken.length > 0) return { authorization: `Bearer ${bearerToken}` };
  if (apiKey && apiKey.trim().length > 0) return { 'x-api-key': apiKey.trim() };
  throw new Error('MCP client must provide either a Better Auth bearer token or an x-api-key.');
}

Type guard

function hasMcpCredentials(opts: { apiKey?: string; bearerToken?: string }): boolean {
  return (!!opts.bearerToken && opts.bearerToken.length > 0) || (!!opts.apiKey && opts.apiKey.trim().length > 0);
}

Prevention

When it happens

Trigger: Calling any MCP endpoint (e.g. POST to the MCP transport route mounted in apps/server) with no Authorization and no x-api-key header; sending an expired/revoked Better Auth session bearer token; sending a malformed or deleted API key in x-api-key; sending a key with the wrong header name (e.g. 'api-key' instead of 'x-api-key').

Common situations: Local dev without logging in first; MCP client config (claude_desktop_config.json / cursor mcp config) missing the apiKey field; the user logged out and the cached bearer token expired; Better Auth API key was rotated or deleted server-side; reverse proxy stripping the Authorization header.

Understand the failure class

Related errors


AI-assisted analysis of amruthpillai/reactive-resume@3a5b12e2a4 (2026-08-12). Data as JSON: /api/errors/0369b23c5843b0c5. Report an issue: GitHub.