# OAuth Auth Proxy for Apollo MCP Server

## Problem

The current Apollo MCP Server setup uses a static JWT in `.env` that expires every ~20 minutes, requiring manual copy-paste from browser DevTools. We want the same browser-based auth experience that Snowflake and Notion MCPs provide — authenticate once in the browser, and Claude Code uses the token automatically with refresh support.

## How Other MCPs Handle Auth

| MCP Server | Auth Method | How It Works |
|------------|-------------|--------------|
| **Snowflake** | `externalbrowser` | Native driver authenticator opens browser → SSO → credentials cached locally |
| **Notion** | Static API token | Manual token from developer portal, stored in Claude Code config |
| **ows-coda** (internal) | OAuth 2.1 proxy to Auth0 | Full MCP OAuth spec — Claude Code auto-discovers endpoints, opens browser, user signs in with Grass/Auth0 credentials, gets JWT + refresh token |

The ows-coda pattern is the gold standard for our use case. [Design doc](https://www.notion.so/32f97177520f8104a03dd91263da7126).

## Why Apollo MCP Server Can't Do This Alone

Apollo MCP Server (Docker image) supports:
- Static headers via env vars (`${env.GRAPHQL_TOKEN}`) — what we use now
- `forward_headers` for dynamic client headers — but docs warn against using for credentials
- `transport.auth` for OAuth 2.1 token **validation** — validates incoming tokens, doesn't initiate login

It **cannot** initiate a browser-based Auth0 login flow on its own.

## Solution: OAuth Auth Proxy

Build a lightweight Node.js proxy that sits between Claude Code and the Apollo MCP Server. The proxy handles the Auth0 OAuth dance and injects the JWT into Apollo MCP requests.

### Architecture

```
Claude Code  --(MCP/HTTP)-->  Auth Proxy (:3000)  --(MCP/HTTP)-->  Apollo MCP Server (:8000)
                                    |
                                    |-- OAuth flow --> Auth0 (qa-orchard)
                                    |<-- JWT + refresh_token
```

### Authentication Flow

```
Claude Code            Auth Proxy (:3000)           Auth0 (qa-orchard)
    |                       |                           |
    |-- GET /.well-known/ ->|                           |
    |<-- auth endpoints ----|                           |
    |                       |                           |
    |-- GET /oauth/authorize -------->|                 |
    |                       |-- redirect to Auth0 ----->|
    |<---- browser opens Auth0 login -------------------|
    |      (user signs in with Grass credentials)       |
    |<---- redirect back with auth code ----------------|
    |                       |                           |
    |-- POST /oauth/token ->|                           |
    |                       |-- exchange code w/ Auth0 >|
    |<-- { access_token } --|<-- JWT + refresh_token ---|
    |                       |                           |
    |-- POST /mcp (Bearer) >|                           |
    |                       |-- inject JWT + headers -->| Apollo MCP (:8000)
    |<-- MCP response ------|<-- response --------------|
```

### Identity Extraction from JWT

The Auth0 JWT contains Grass identity in the `https://grass.theorchard.com/identity` claim. The proxy extracts and injects these as headers:

```
authorization: Bearer <jwt>
orchard-identity-id: <from JWT claim>
orchard-identity-uuid: <from JWT claim>
orchard-profile-id: <from JWT claim>
orchard-profile-type: <from JWT claim>
orchard-profile-uuid: <from JWT claim>
apollographql-client-name: frontend-insights
```

This is identical to how ows-coda and ows-grass inject identity headers.

## Implementation

### Project Structure

```
auth-proxy/
├── package.json
├── tsconfig.json
├── .env.example           # Auth0 domain, client_id, audience
├── src/
│   ├── index.ts           # Express server, mounts routes
│   ├── oauth-metadata.ts  # GET /.well-known/oauth-authorization-server
│   ├── authorize.ts       # GET /oauth/authorize → redirect to Auth0
│   ├── callback.ts        # GET /oauth/callback → Auth0 redirect back
│   ├── token.ts           # POST /oauth/token → exchange with Auth0
│   ├── proxy.ts           # POST /mcp → inject JWT, forward to Apollo MCP
│   └── token-store.ts     # In-memory token storage + auto-refresh timer
```

### Step 1: OAuth Metadata Discovery

`GET /.well-known/oauth-authorization-server` returns:

```json
{
  "issuer": "http://127.0.0.1:3000",
  "authorization_endpoint": "http://127.0.0.1:3000/oauth/authorize",
  "token_endpoint": "http://127.0.0.1:3000/oauth/token",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"]
}
```

Claude Code discovers these endpoints automatically per the MCP Authorization spec.

### Step 2: Authorization Code + PKCE Flow

1. Claude Code redirects to `GET /oauth/authorize` with PKCE params
2. Proxy constructs Auth0 `/authorize` URL with:
   - `audience`: `https://qa-ows.theorchard.io`
   - `scope`: `openid profile email offline_access`
   - `code_challenge`: from Claude Code's PKCE
   - `redirect_uri`: `http://127.0.0.1:3000/oauth/callback`
3. User signs in at Auth0 with Grass credentials
4. Auth0 redirects back with authorization code
5. Proxy stores the code, returns it to Claude Code

### Step 3: Token Exchange

`POST /oauth/token` — Claude Code sends the auth code + PKCE verifier:
1. Proxy exchanges with Auth0's `/oauth/token`
2. Gets back `access_token` (JWT) + `refresh_token` + `expires_in`
3. Stores tokens in memory
4. Sets up auto-refresh timer (`expires_in - 60s`)
5. Returns tokens to Claude Code

### Step 4: MCP Request Proxying

All `POST /mcp` requests:
1. Validate Bearer token from Claude Code
2. Decode JWT, extract Grass identity from claims
3. Forward to `http://127.0.0.1:8000/mcp` with:
   - Original MCP JSON-RPC body
   - Injected `Authorization: Bearer <jwt>` + Orchard identity headers
4. Stream response back to Claude Code

### Step 5: Token Auto-Refresh

- On startup, check if stored refresh token exists
- Before access token expiry, call Auth0 `/oauth/token` with `grant_type=refresh_token`
- Update stored tokens
- If refresh fails, next MCP request returns 401 → Claude Code re-triggers OAuth flow

### Step 6: Simplified Apollo MCP Config

Remove static auth headers from `mcp.yaml` since the proxy injects them:

```yaml
endpoint: "${env.GRAPHQL_ENDPOINT}"

transport:
  type: streamable_http
  port: 8000

headers:
  apollographql-client-name: frontend-insights
  # authorization + identity headers injected by auth-proxy

schema:
  source: local
  path: /schema.graphql

introspection:
  execute:
    enabled: true
  introspect:
    enabled: true
    minify: true
  search:
    enabled: true
    minify: true
  validate:
    enabled: true

operations:
  source: local
  paths:
    - /operations/

overrides:
  mutation_mode: explicit
```

### Step 7: Claude Code Config

```json
{
  "mcpServers": {
    "apollo-qa": {
      "type": "streamable-http",
      "url": "http://127.0.0.1:3000/mcp"
    }
  }
}
```

No `mcp-remote` bridge needed — Claude Code talks directly to the proxy.

## Auth0 Configuration

### Required: Auth0 Application

Create (or reuse) an application in the `qa-orchard` Auth0 tenant:

- **Type**: Single Page Application or Native
- **Allowed Callback URLs**: `http://127.0.0.1:3000/oauth/callback`
- **Allowed Logout URLs**: `http://127.0.0.1:3000`
- **Grant Types**: Authorization Code, Refresh Token
- **Token Endpoint Auth Method**: None (public client, PKCE-only)

### Auth0 API

- **Audience**: `https://qa-ows.theorchard.io` (existing)
- **Scopes**: Standard Grass scopes — `openid profile email offline_access`

### Environment Variables

```bash
# auth-proxy/.env
AUTH0_DOMAIN=qa-orchard.us.auth0.com
AUTH0_CLIENT_ID=<your-client-id>
AUTH0_AUDIENCE=https://qa-ows.theorchard.io
AUTH0_CALLBACK_URL=http://127.0.0.1:3000/oauth/callback
APOLLO_MCP_URL=http://127.0.0.1:8000/mcp
PORT=3000
```

## Running

```bash
# Terminal 1: Start Apollo MCP Server (unchanged)
docker run -it --rm --name apollo-mcp -p 8000:8000 \
  --env-file .env \
  -v $(pwd)/mcp.yaml:/config.yaml \
  -v $(pwd)/operations:/operations \
  -v $(pwd)/schema.graphql:/schema.graphql \
  ghcr.io/apollographql/apollo-mcp-server:v1.11.0 /config.yaml

# Terminal 2: Start auth proxy
cd auth-proxy && npm run dev

# Terminal 3: Use Claude Code — first use triggers browser OAuth
claude
```

## Verification Checklist

- [ ] Auth proxy starts on :3000
- [ ] `GET /.well-known/oauth-authorization-server` returns valid metadata
- [ ] Claude Code discovers auth endpoints and opens browser
- [ ] User can sign in with Grass credentials at Auth0
- [ ] JWT is obtained and stored
- [ ] MCP tools work (introspect, search, execute)
- [ ] Token auto-refreshes before expiry
- [ ] After refresh token expiry, re-auth flow triggers correctly

## References

- [ows-coda MCP Server Design](https://www.notion.so/32f97177520f8104a03dd91263da7126) — the auth flow pattern we're replicating
- [Auth0 M2M Token Guide](https://www.notion.so/2e00d3e763ce497abaf6d2e9a0e947ff) — JWT claims and identity header format
- [Auth0 Architecture](https://www.notion.so/8371780310fa4c8aa7df8574e1ef87c4) — Auth0 tenant setup, connections, JWT hydration
- [Apollo MCP Server Docs](https://www.apollographql.com/docs/apollo-mcp-server) — config reference
- [MCP Authorization Spec](https://spec.modelcontextprotocol.io/specification/2025-03-26/basic/authorization/) — OAuth 2.1 discovery and flow
