Chris Farhood 46bf005ec3
build / test (push) Failing after 12m52s
build / build (push) Has been skipped
cleanup: remove DEBUG_EMAIL_PASSWORD email/password backdoor; add MIT LICENSE + .env.example
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 07:53:53 -04:00

intervalsicu-mcp-auth

The OAuth 2.1 / OIDC authorization server (identity provider) for the self-hosted, multi-tenant Intervals.icu MCP product. Built on Better Auth (TypeScript, plain Node HTTP server), served at https://intervalsicu.farhoodlabs.com/api/auth.

Role in the system

Three services share one host and one CloudNativePG Postgres database:

  • intervalsicu-mcp — Python/FastMCP MCP server at /mcp. Verifies this server's EdDSA-signed JWT access tokens against its JWKS, then resolves per-user Intervals.icu credentials by the token sub.
  • intervalsicu-mcp-ui — Python portal at /portal. An OIDC client of this auth server.
  • intervalsicu-mcp-auth (this repo) — the authorization server both depend on.

Users sign in with Google (Apple optional). This server:

  • Provides Dynamic Client Registration (RFC 7591) so Claude's MCP connector self-registers.
  • Uses PKCE.
  • Issues EdDSA-signed JWT access tokens with a JWKS endpoint, so the MCP server verifies them RFC 9068 style (with aud = the requested resource).
  • The token sub is this service's user id — the shared identity the MCP server and portal both key per-user Intervals credentials on.

Layout

  • src/auth.ts — Better Auth config: jwt() + oauthProvider() plugins, Google/Apple social providers (env-gated), shared pg.Pool.
  • src/server.ts — Node HTTP server; custom /login, /consent, /healthz routes, everything else delegated to Better Auth.
  • src/migrate.ts — applies Better Auth's schema and seeds the portal OIDC client (run as a k8s initContainer).

See CLAUDE.md for the detailed architecture (why jwt() comes before oauthProvider(), why validAudiences matters, and why /login and /consent are custom routes).

Development

npm ci                 # install
npm run typecheck      # tsc --noEmit (the CI gate; there are no tests)
npm run build          # tsc -> dist/
npm run dev            # tsx watch src/server.ts (hot reload)
npm run migrate        # apply schema + seed the portal client
npm start              # node dist/server.js

Key environment variables

Var Notes
BETTER_AUTH_URL required — public base URL.
BETTER_AUTH_SECRET required — signing secret.
AUTH_DATABASE_URL required — Postgres (shared CNPG DB).
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET enable Google login.
APPLE_CLIENT_ID / APPLE_CLIENT_SECRET enable Apple login (optional).
VALID_AUDIENCES comma-separated allowed token audiences / RFC 8707 resources (has default).
TRUSTED_ORIGINS comma-separated trusted origins.
LOG_LEVEL debug / info (default info).
PORT HTTP port (default 8080).
PORTAL_CLIENT_ID / PORTAL_REDIRECT_URI portal client seeded by migrate.ts (have defaults).

Deployment

Push to main triggers Gitea CI (git.farh.net): typecheck, then build and push the Docker image to git.farh.net/farhoodlabs/intervalsicu-mcp-auth. Flux (GitOps) deploys it on Kubernetes, running node dist/migrate.js as an initContainer. Served under /api/auth (+ /login, /consent) behind the cluster gateway.

S
Description
No description provided
Readme MIT 136 KiB
Languages
TypeScript 98.3%
Dockerfile 1.7%