Stu Mason
Stu Mason

Activity

Pull Request Opened

PR #165 opened: MCP auth: OAuth 2.1 via Passport (replaces static token)

Why

The static bearer token was a dead end — claude.ai/Cursor/VS Code remote connectors authenticate via OAuth (discovery + dynamic client registration), not pasted secrets. This wires real OAuth 2.1 on the existing [email protected] account.

How it fits together

  • Passport = the OAuth2 authorization server (oauth/authorize, oauth/token — auto-registered).
  • laravel/mcp = the MCP-side glue: Mcp::oauthRoutes() publishes .well-known/oauth-* discovery + oauth/register (dynamic client registration); its built-in AddWwwAuthenticateHeader turns the guard's 401 into an OAuth discovery challenge so clients start the flow automatically.
  • /mcp/devtrends is now guarded by auth:api (Passport). Static McpAuthenticate + devtrends.mcp_token deleted.

Changes

  • composer require laravel/passport (13.7) + published oauth_* migrations.
  • User implements OAuthenticatable + HasApiTokens; new api passport guard.
  • routes/mcp.php rewired; config/mcp.php allows claude/cursor/vscode schemes.
  • Fortify public registration disabled (single private account).
  • CI gets a passport:keys step.

⚠️ Deploy runbook (required — OAuth won't work without this)

  1. Generate keys once and set them as Coolify env vars (they must persist across container rebuilds — Passport reads them from env):
    php artisan passport:keys          # generates storage/oauth-private.key + oauth-public.key
    
    Then in Coolify, set:
    • PASSPORT_PRIVATE_KEY = full contents of oauth-private.key (multiline, incl. BEGIN/END lines)
    • PASSPORT_PUBLIC_KEY = full contents of oauth-public.key
  2. Migrations run automatically on deploy (AUTO_MIGRATE=true) — creates the oauth_* tables.
  3. Ensure APP_URL=https://trends.stumason.dev (the OAuth issuer/discovery URLs derive from it).
  4. Reconnect the client to https://trends.stumason.dev/mcp/devtrends — it'll discover OAuth, send you to log in (your account), you consent, done. No pasted secrets.

Verification

  • 142 tests pass on Postgres: unauthed MCP → 401 + WWW-Authenticate discovery; .well-known metadata points at Passport; authed user passes the guard.
  • route:list shows the full chain: .well-known/oauth-*oauth/registeroauth/authorize + oauth/token → guarded mcp/devtrends.

Note

Keys are gitignored (/storage/*.key) — never committed. Generate prod keys fresh; don't reuse dev keys.

+1356
additions
-235
deletions
22
files changed