Skip to content

feat: OAuth 2.0 gateway for Claude.ai web connectivity#31

Open
fjprobos wants to merge 2 commits intoCognitionAI:mainfrom
madeofclay:feat/oauth-gateway
Open

feat: OAuth 2.0 gateway for Claude.ai web connectivity#31
fjprobos wants to merge 2 commits intoCognitionAI:mainfrom
madeofclay:feat/oauth-gateway

Conversation

@fjprobos
Copy link
Copy Markdown

Why this is needed

The HTTP Stream transport (PR #30) requires clients to pass Metabase credentials as request headers. Web-based clients like Claude.ai and Claude Code web do not support custom headers — they require a standard OAuth 2.0 flow. This gateway bridges that gap.

Client HTTP Stream (PR #30) OAuth Gateway (this PR)
Claude Code CLI --header flags
Claude Desktop ✅ stdio
Claude.ai web ❌ no header support
Claude Code web ❌ no header support

How it works

  1. Client discovers the gateway via /.well-known/oauth-authorization-server
  2. User is redirected to /oauth/authorize — an HTML form asking for Metabase URL + API key (or username/password)
  3. On submit, server stores credentials under a short-lived auth code and redirects back to the client
  4. Client exchanges the code at /oauth/token for a signed JWT (10-min TTL on code, configurable token expiry)
  5. All /mcp requests carry Authorization: Bearer <JWT>
  6. Gateway validates the JWT, injects x-metabase-* headers, and proxies to the upstream HTTP Stream server

Usage

# Start the upstream HTTP Stream server
MCP_TRANSPORT=http node dist/server.js

# Start the OAuth gateway
GATEWAY_URL=https://mcp.example.com \
GATEWAY_PORT=8080 \
MCP_UPSTREAM=http://localhost:8011 \
JWT_SECRET=your-random-secret \
node dist/oauth-gateway.js

Then add to Claude.ai / Claude Code web:

https://mcp.example.com/mcp

The user will be prompted with a login form on first connection.

Security

  • Credentials are never logged (only truncated session ID + HTTP status)
  • Client registration details only logged at LOG_LEVEL=debug
  • Auth codes are single-use with a 10-minute TTL
  • PKCE (S256) support for public clients
  • JWT signed with configurable JWT_SECRET

Environment variables

Variable Default Description
GATEWAY_URL http://localhost:8080 Public base URL of the gateway
GATEWAY_PORT 8080 Port to listen on
MCP_UPSTREAM http://localhost:8011 Upstream HTTP Stream server URL
JWT_SECRET (random — changes on restart) Secret for signing JWT tokens. Must be set in production.
TOKEN_EXPIRY 8h JWT expiry (e.g. 1h, 24h)
LOG_LEVEL info Set to debug for verbose logging

Tests (28 passing)

  • OAuth discovery endpoints (/.well-known/oauth-authorization-server, /.well-known/openid-configuration)
  • Dynamic client registration (RFC 7591)
  • Authorization endpoint: form rendering, XSS escaping, input validation
  • Token endpoint: api-key flow, username+password flow, single-use code enforcement
  • PKCE (S256): correct verifier ✅, wrong verifier ❌, missing verifier ❌
  • MCP proxy: 401 without token, 401 with invalid/expired token

Test plan

  • node dist/oauth-gateway.js starts on configured port
  • GET /.well-known/oauth-authorization-server returns discovery document
  • Claude.ai web connects and shows the Metabase login form
  • After login, MCP tools are available
  • Invalid/expired tokens return 401
  • npm test — 28 tests pass

🤖 Generated with Claude Code

piero-clay and others added 2 commits March 13, 2026 18:50
Adds an OAuth 2.0 Authorization Code + PKCE gateway so that web-based
MCP clients (Claude.ai, Claude Code web) can connect without pre-sharing
credentials via request headers.

## Why this is needed

The HTTP Stream transport (PR CognitionAI#30) requires clients to pass Metabase
credentials as request headers. Web clients like Claude.ai do not support
custom headers — they require a standard OAuth 2.0 flow. This gateway
bridges that gap.

## How it works

1. Client discovers the gateway via `/.well-known/oauth-authorization-server`
2. User is redirected to `/oauth/authorize` — an HTML form asking for
   Metabase URL + API key (or username/password)
3. On submit, server stores credentials under a short-lived auth code
   and redirects back to the client
4. Client exchanges the code at `/oauth/token` for a signed JWT
5. All `/mcp` requests carry `Authorization: Bearer <JWT>`
6. Gateway validates the JWT, injects `x-metabase-*` headers, and
   proxies to the upstream HTTP Stream server

## Security

- Credentials are never logged (only session prefix + status)
- Client registration details logged only at LOG_LEVEL=debug
- Auth codes are single-use with a 10-minute TTL
- PKCE (S256) support for public clients
- JWT signed with configurable JWT_SECRET

## Tests (28 passing)

- OAuth discovery endpoints
- Dynamic client registration (RFC 7591)
- Authorization endpoint: form rendering, XSS escaping, validation
- Token endpoint: api-key flow, username/password flow, single-use codes
- PKCE: correct verifier, wrong verifier, missing verifier
- MCP proxy: 401 without token, 401 with invalid token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Intercept notifications/initialized at gateway level and return 202
  instead of proxying (upstream returns 400 in stateless mode)
- Refactor proxy into doProxy(attempt) with one retry on 400 for
  non-initialize methods (60ms delay, handles stateless race condition)
- Add @types/express devDependency
- Normalize log level strings to lowercase

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to 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.

2 participants