Description
When a deployment turns on JWT auth, API-key auth is now automatically disabled, so it stops accepting API keys without a second toggle. This is the behavior an enterprise/self-hosted customer migrating to JWT expects: once JWT is on, a leaked or legacy API key no longer works.
Before: api_key, org_api_key, and jwt were fully independent entries in DISABLE_AUTH_MODES. Enabling JWT (setting AUTH_JWT_JWKS_URL plus AUTH_JWT_ISSUER) left API keys fully functional until the operator also added api_key (and org_api_key) to DISABLE_AUTH_MODES. That was easy to forget, so API keys kept working alongside JWT.
After: two helpers in jwt_auth.ts couple the modes:
isApiKeyAuthModeEnabled() = isAuthModeEnabled('api_key') && !isJwtAuthModeEnabled()
isOrgApiKeyAuthModeEnabled() = isAuthModeEnabled('org_api_key') && !isJwtAuthModeEnabled()
Every request-auth gate routes through them: authMiddleware.ts (project/user key paths, two sites), resolvers.ts (MCP/tool-router project key), addOrgAuthInfo.ts (org key), and the with_toolrouter_v2_auth.ts error-message selector. So with JWT enabled, ak_ / uak_ / oak_ are all rejected (401), and Authorization: Bearer ak_... no longer falls back to API-key validation after JWT verification fails.
Why this is safe for cloud and non-JWT deployments: JWT mode is strictly opt-in. isJwtAuthModeEnabled() returns false unless BOTH AUTH_JWT_JWKS_URL and AUTH_JWT_ISSUER are set (both optional, no default) AND jwt is not in DISABLE_AUTH_MODES. Any deployment that never configures a JWT verifier (including Composio cloud) is completely unaffected: isJwtAuthModeEnabled() is false, so the helpers reduce to the previous isAuthModeEnabled('api_key') behavior.
Admin-token and cookie auth are intentionally untouched. Operators who explicitly disable jwt in DISABLE_AUTH_MODES (even with verifier config present) keep API keys working.
Files
apps/apollo/src/lib/jwt_auth.ts: add isApiKeyAuthModeEnabled() and isOrgApiKeyAuthModeEnabled().
apps/apollo/src/lib/authMiddleware.ts: route both isApiKeyEnabled derivations through the helper.
apps/apollo/src/server/nextjs/resolver/resolvers.ts: same for the project-key gate.
apps/apollo/src/server/middleware/authInfo/addOrgAuthInfo.ts: org-key gate via isOrgApiKeyAuthModeEnabled().
apps/apollo/src/server/nextjs/middleware/with_toolrouter_v2_auth.ts: bearer-only error message keys off the coupled helper.
How did I test this PR
- Added a
describe('API-key auth coupling with JWT') block in jwt_auth.test.ts covering: API keys accepted when no JWT verifier is configured; project/user AND org keys disabled once JWT is enabled; keys re-enabled when jwt is explicitly in DISABLE_AUTH_MODES despite verifier config present; and api_key/org_api_key disable still winning when JWT is also off.
tsgo --noEmit is clean for all touched files.
- Verified the gate is universal: grep confirms no remaining raw
isAuthModeEnabled('api_key') / ('org_api_key') call sites outside the two helpers, so no auth path is left on the old uncoupled check.
- Full vitest suite runs in CI (needs Doppler env).