diff --git a/CLAUDE.md b/CLAUDE.md index d7aeeab..8946cab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,3 +67,34 @@ template rendering). 3. **No test frameworks**: stdlib `testing` only, real SQLite in t.TempDir(). 4. **CSRF on all web mutations**: double-submit cookie pattern. 5. **Session cookies**: HttpOnly, Secure, SameSite=Strict. + +## SSO (public vs internal MCIAS URLs) + +MCQ is reachable publicly (`mcq.metacircular.net`), so its SSO uses **two** +MCIAS URLs (via `mcdsl/sso` ≥ v1.9.0): + +- `[mcias].server_url` — the **internal** address (`https://mcias.svc.mcp.metacircular.net:8443`) + used for the server-to-server authorization-code exchange. Efficient and + does not depend on the public edge. +- `[sso].public_url` — the **public, browser-facing** MCIAS base URL + (`https://mcias.metacircular.net`) used to build the authorize redirect, so + end-user browsers (which can't resolve the internal name) can reach it. +- `[sso].redirect_uri` must be the **public** callback + (`https://mcq.metacircular.net/sso/callback`) and must match the + `redirect_uri` registered for the `mcq` SSO client in MCIAS + (`mciasctl sso update --client-id mcq --redirect-uri ...`). + +If `public_url` is empty the authorize redirect falls back to `server_url` +(tailnet-only SSO). The startup log prints both `authorize_url` and +`exchange_url` so you can confirm the split. + +## Deployment / runtime + +- **Containers run rootless under MCP.** Dockerfiles must NOT declare + `VOLUME /srv/mcq`, pre-create/chown the data dir, or set `USER` — MCP + bind-mounts `/srv/mcq` and runs `--user 0:0`. See + `../engineering-standards.md` → Containerization. +- **Not unikernel-eligible (yet).** MCQ writes a SQLite DB to `/srv/mcq`; + the unikernel runtime currently bakes config/certs read-only and has no + writable host mount, so MCQ stays a container until 9p/virtio-blk storage + lands. See `docs/unikernels.md` in the workspace root. diff --git a/cmd/mcq/server.go b/cmd/mcq/server.go index 87d9296..eb70597 100644 --- a/cmd/mcq/server.go +++ b/cmd/mcq/server.go @@ -78,6 +78,7 @@ func runServer(configPath string) error { ServiceName: cfg.MCIAS.ServiceName, Tags: cfg.MCIAS.Tags, MciasURL: cfg.MCIAS.ServerURL, + PublicURL: cfg.SSO.PublicURL, CACert: cfg.MCIAS.CACert, RedirectURI: cfg.SSO.RedirectURI, } diff --git a/go.mod b/go.mod index 45b5b24..f0ecc80 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.wntrmute.dev/mc/mcq go 1.25.7 require ( - git.wntrmute.dev/mc/mcdsl v1.7.0 + git.wntrmute.dev/mc/mcdsl v1.9.0 github.com/alecthomas/chroma/v2 v2.18.0 github.com/go-chi/chi/v5 v5.2.5 github.com/mark3labs/mcp-go v0.46.0 diff --git a/go.sum b/go.sum index c842129..a37aeee 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.wntrmute.dev/mc/mcdsl v1.7.0 h1:dAh2SGdzjhz0H66i3KAMDm1eRYYgMaxqQ0Pj5NzF7fc= -git.wntrmute.dev/mc/mcdsl v1.7.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc= +git.wntrmute.dev/mc/mcdsl v1.9.0 h1:TGqVhf9uhhh5jpMhN+8eNtBPSi/wwNXQn/NFDAcU4wg= +git.wntrmute.dev/mc/mcdsl v1.9.0/go.mod h1:MhYahIu7Sg53lE2zpQ20nlrsoNRjQzOJBAlCmom2wJc= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= diff --git a/internal/config/config.go b/internal/config/config.go index 3c4c924..f50e985 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,8 +24,16 @@ type Config struct { // SSOConfig holds SSO redirect settings for the web UI. type SSOConfig struct { // RedirectURI is the callback URL that MCIAS redirects to after login. - // Must exactly match the redirect_uri registered in MCIAS config. + // Must exactly match the redirect_uri registered in MCIAS config. For + // public (non-Tailnet) browser access this must be the public hostname. RedirectURI string `toml:"redirect_uri"` + + // PublicURL is the browser-facing MCIAS base URL used to build the SSO + // authorize redirect (e.g. "https://mcias.metacircular.net"). When empty, + // the backend [mcias].server_url is used for the redirect too. Set this + // when browsers cannot resolve the internal MCIAS name; the + // server-to-server code exchange still uses [mcias].server_url. + PublicURL string `toml:"public_url"` } // ServerConfig holds HTTP/gRPC server settings. TLS fields are optional; diff --git a/internal/webserver/server.go b/internal/webserver/server.go index 29b8bfe..dcb0646 100644 --- a/internal/webserver/server.go +++ b/internal/webserver/server.go @@ -28,7 +28,8 @@ type Config struct { Tags []string // SSO fields — when RedirectURI is non-empty, the web UI uses SSO instead // of the direct username/password login form. - MciasURL string + MciasURL string // internal MCIAS URL for the server-to-server code exchange + PublicURL string // browser-facing MCIAS URL for the authorize redirect (optional) CACert string RedirectURI string } @@ -65,6 +66,7 @@ func New(cfg Config, database *db.DB, authenticator *auth.Authenticator, logger if cfg.RedirectURI != "" { ssoClient, err := mcdsso.New(mcdsso.Config{ MciasURL: cfg.MciasURL, + PublicURL: cfg.PublicURL, ClientID: "mcq", RedirectURI: cfg.RedirectURI, CACert: cfg.CACert, @@ -73,7 +75,12 @@ func New(cfg Config, database *db.DB, authenticator *auth.Authenticator, logger return nil, fmt.Errorf("create SSO client: %w", err) } s.ssoClient = ssoClient - logger.Info("SSO enabled: redirecting to MCIAS for login", "mcias_url", cfg.MciasURL) + authorizeURL := cfg.PublicURL + if authorizeURL == "" { + authorizeURL = cfg.MciasURL + } + logger.Info("SSO enabled: redirecting to MCIAS for login", + "authorize_url", authorizeURL, "exchange_url", cfg.MciasURL) } return s, nil