Files
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

70 lines
3.2 KiB
Markdown

# 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.