# 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](https://better-auth.com) (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`](./CLAUDE.md) for the detailed architecture (why `jwt()` comes before `oauthProvider()`, why `validAudiences` matters, and why `/login` and `/consent` are custom routes). ## Development ```bash 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.