Skip to content

Harden hosted MCP OAuth docs, challenge semantics, and e2e smoke tooling - #179

Closed
dodeja wants to merge 0 commit into
mainfrom
codex/data-8540-workos-only-mcp-auth
Closed

Harden hosted MCP OAuth docs, challenge semantics, and e2e smoke tooling#179
dodeja wants to merge 0 commit into
mainfrom
codex/data-8540-workos-only-mcp-auth

Conversation

@dodeja

@dodejadodeja commented Feb 28, 2026

Copy link
Copy Markdown
Member

Greptile Summary

This PR implements OAuth 2.1 authentication for the hosted MCP server while maintaining backward compatibility with legacy API token authentication. The implementation adds JWT verification with WorkOS, proper WWW-Authenticate challenge semantics per RFC standards, and comprehensive e2e smoke testing tooling.

Key Changes:

  • Added workos-jwt.ts module with RS256 signature verification, JWKS caching with key rotation support, and comprehensive claim validation (issuer, audience, scope, exp, nbf)
  • Updated MCP handler to detect JWT-like Bearer tokens and verify them via local WorkOS JWT validation or remote internal principal verification fallback
  • Implemented RFC-compliant OAuth challenge response with WWW-Authenticate: Bearer resource_metadata=... headers on 401s
  • Updated TypeScript SDK client to preserve Bearer/Token prefixes for OAuth and legacy tokens
  • Added comprehensive test coverage for OAuth scenarios including challenge headers and internal verification fallback
  • Created bash script for OAuth E2E smoke testing covering discovery, registration, and authenticated MCP calls
  • Added three new documentation files: OAuth requirements specification, test plan with traceability matrix, and E2E smoke test runbook
  • Updated existing docs to position OAuth as the preferred authentication method while documenting legacy compatibility

Security & Quality:

  • JWT verification follows security best practices: algorithm validation (RS256 only), kid validation, signature verification with public keys from JWKS, claim validation (exp, nbf, iss, aud, scope)
  • Dual-mode authentication allows gradual migration from API tokens to OAuth
  • Comprehensive test coverage added for new OAuth flows
  • Proper error handling throughout with graceful fallbacks

The implementation is solid, well-tested, and follows OAuth 2.1 and RFC standards for MCP authentication.

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • The implementation follows OAuth 2.1 and JWT security best practices, includes comprehensive test coverage for new scenarios, maintains backward compatibility during migration, and adds extensive documentation. No critical bugs or security vulnerabilities identified. The code is well-structured with proper error handling and follows established patterns.
  • No files require special attention

Important Files Changed

FilenameOverview
api/mcp.tsadds OAuth JWT verification with local and remote fallback, WWW-Authenticate challenge headers, and maintains backward compatibility with legacy API tokens
packages/mcp/src/auth/workos-jwt.tsnew JWT verification module with RS256 signature validation, JWKS caching, comprehensive claim validation (exp, nbf, iss, aud, scope), and proper error handling
sdks/typescript-sdk/src/client.tsupdated to preserve Bearer-prefixed tokens (OAuth JWTs) alongside Token-prefixed legacy API tokens
packages/mcp/tests/api-handler.test.tsadded OAuth challenge header validation tests and internal fallback verification test coverage
scripts/mcp-oauth-e2e-smoke.shnew bash script for OAuth smoke testing covering discovery, registration, challenge semantics, and authenticated MCP calls

Sequence Diagram

sequenceDiagram
participant Client as MCP Client
participant MCP as MCP Handler
participant WorkOS as WorkOS JWT
participant Internal as Internal Verify
participant Upstream as Terminal49 API
Note over Client,Upstream: OAuth Flow (JWT Bearer Token)
Client->>MCP: POST /mcp (no auth)
MCP->>Client: 401 + WWW-Authenticate header
Note right of Client: Discovery + OAuth flow happens<br/>(external to this PR)
Client->>MCP: POST /mcp (Bearer JWT)
MCP->>MCP: Detect JWT-like Bearer token
MCP->>WorkOS: Verify JWT locally
alt Local JWT valid
WorkOS->>MCP: Valid (user_id, account_id)
MCP->>Upstream: Call API with Bearer token
Upstream->>MCP: Response
MCP->>Client: 200 OK
else Local JWT invalid
WorkOS->>MCP: Invalid
MCP->>Internal: Verify via internal endpoint
alt Internal verify succeeds
Internal->>MCP: Valid (active, user_id, account_id)
MCP->>Upstream: Call API with Bearer token
Upstream->>MCP: Response
MCP->>Client: 200 OK
else Both verifications fail
Internal->>MCP: Invalid
MCP->>Client: 401 + WWW-Authenticate
end
end
Note over Client,Upstream: Legacy Flow (API Token)
Client->>MCP: POST /mcp (Token <api_token>)
MCP->>MCP: Not JWT-like, check client secret
MCP->>Upstream: Call API with configured token
Upstream->>MCP: Response
MCP->>Client: 200 OK
Loading

Last reviewed commit: d740f6c

@vercel

vercelBot commented Feb 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
apiReadyReadyPreview, CommentFeb 28, 2026 4:50pm

Request Review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:d740f6c30f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadapi/mcp.ts Outdated
let authSource: 'authorization' | 'environment' | 'oauth_local' | 'oauth_remote' =
resolvedAuth.source ?? 'authorization';

const jwtLikeBearerToken = callerScheme === 'bearer' && looksLikeJwt(callerToken);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept opaque Bearer tokens in OAuth validation path

The OAuth branch is gated on looksLikeJwt, so only Bearer tokens with exactly three dot-separated segments are treated as OAuth tokens. Any valid opaque OAuth access token will skip this path and be handled as legacy auth, which means it is rejected as Invalid client credentials whenever T49_API_TOKEN/T49_MCP_CLIENT_SECRET is configured (or forwarded upstream as a Token scheme when not configured). This breaks hosted OAuth clients that receive opaque access tokens.

Useful? React with 👍 / 👎.

Comment threadpackages/mcp/src/auth/workos-jwt.ts Outdated
}
}

const response = await fetch(jwksUrl, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle JWKS fetch failures as auth failures

fetchJwks performs network I/O without error handling, so a transient JWKS network error (DNS, timeout, connection reset) throws out of verifyWorkosJwt instead of returning null. In api/mcp.ts, that propagates to the top-level catch and returns HTTP 500, which both skips internal token verification fallback and turns a token-validation issue into a server error for clients.

Useful? React with 👍 / 👎.

@dodejadodeja closed this Jul 1, 2026
@dodeja
dodejaforce-pushed the codex/data-8540-workos-only-mcp-auth branch from d740f6c to 8f6e499CompareJuly 1, 2026 20:45
@dodeja

Copy link
Copy Markdown
MemberAuthor

Rebase result: this PR is fully superseded by #240 (already merged to main)

I rebased this branch onto current main to resolve the DIRTY merge state and the failing mcp CI check. After careful review, every change in this PR's diff is now obsolete — main shipped a different (and RFC-compliant) OAuth architecture in #240 ("Add WorkOS MCP auth gateway") back in June, well after this PR (Feb 28) was opened. Specifically:

  • This PR's core mechanism — local WorkOS JWT verification in the gateway (packages/mcp/src/auth/workos-jwt.ts, WORKOS_MCP_ISSUER/WORKOS_MCP_AUDIENCE/WORKOS_MCP_JWKS_URL, plus a Rails "internal token principal" fallback) — conflicts with the shipped design, where token validation is delegated to the Terminal49 backend via /connected-clients/resolve (see AGENTS.md/CLAUDE.md: "Token validation is delegated to the Terminal49 backend... the backend must enforce the token audience"). Reintroducing local verification would create two divergent trust paths for the same bearer token.
  • This PR's oauthResourceMetadataUrl() derives its own OAuth resource-metadata URL per-file (defaulting to https://api.terminal49.com/.well-known/oauth-authorization-server). Main's shipped fix in Add WorkOS MCP auth gateway #240 explicitly centralized this in packages/mcp/src/resource.ts as the single source of truth specifically so the PRM document and the WWW-Authenticate challenge can never diverge (RFC 9728) — the repo instructions explicitly say not to reintroduce per-file resource derivation.
  • The three new docs (hosted-http-oauth-requirements.mdx, hosted-http-oauth-test-plan.mdx, oauth-e2e-smoke.mdx) and scripts/mcp-oauth-e2e-smoke.sh describe a Rails-hosted OAuth Authorization Server with dynamic client registration (POST /oauth/register) and a Rails-served /.well-known/oauth-authorization-server. That's not what got built — WorkOS is the external Authorization Server, there's no DCR, and the AS metadata is discovered directly from WorkOS via the PRM's authorization_servers field. Publishing these docs as-is would describe a system that doesn't exist.
  • The SDK client changes (preserving Bearer/Token prefixes) and their tests are already implemented equivalently (and more completely) on main.

Failing mcp CI check root cause: the added tests in tests/api-handler.test.ts asserted the old resource_metadata="https://api.terminal49.com/.well-known/oauth-authorization-server" challenge value and an "Invalid OAuth bearer token" message — both from the superseded local-JWT code path. Main's current behavior only advertises resource_metadata via protectedResourceMetadataUrl(req) (resource.ts) when AuthKit + WorkOS are configured, so these assertions no longer match reality.

Review comments: both Greptile findings (opaque-token rejection in the JWT branch, and unhandled JWKS fetch failures) are on packages/mcp/src/auth/workos-jwt.ts, which no longer exists after this reconciliation — the code path they flagged is gone rather than patched.

What I did: rebased onto origin/main, resolved every conflict in favor of main's shipped implementation, and dropped the PR's now-obsolete additions rather than reintroducing a competing/incorrect auth path or misleading docs. The resulting diff against main is empty — npm run test/build/lint --workspace @terminal49/mcp and tsc --noEmit are all green (130/130 tests passing).

Recommendation: close this PR as superseded by #240. Happy to reopen/redo it scoped to anything from here that's still genuinely missing (e.g., dedicated OAuth e2e smoke tooling for the current WorkOS-based flow) if that's still wanted — but it should be written against the shipped architecture, not the original Rails-AS design.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dodeja