Authentication & Users
The Go gateway is the sole auth authority (design decision #18). The Next.js dashboard owns no sessions — it only forwards the gateway's httpOnly cookie. There are two distinct authentication surfaces:
| Surface | Endpoints | Credential |
|---|---|---|
| MCP data plane | /mcp (legacy), /mcp/p/<slug> (per profile) | Bearer tokens |
| Control plane (REST API + dashboard) | /api/* | gig_session cookie issued after OIDC login |
MCP bearer tokens
Legacy gateway token
GIG_BEARER_TOKEN (required at startup) authenticates clients on the legacy aggregated endpoint:
claude mcp add --transport http gigmcp http://localhost:8080 \
--header "Authorization: Bearer <GIG_BEARER_TOKEN>"
/mcp and the bare / catch-all both serve this legacy handler; /mcp/p/ and /api/ take precedence by mux specificity.
Per-profile tokens
Each profile has its own bearer token, prefixed gig_ so leaked tokens are greppable. The plaintext is shown exactly once (on create or rotate); only the SHA-256 hash is stored. Every request to /mcp/p/<slug> re-hashes the presented token and looks it up in the database, so a rotated token is rejected immediately — there is no cached window.
OIDC login (control plane)
The gateway is a generic OIDC client built on coreos/go-oidc. In the current implementation Zitadel is the sole IdP; social login (Google/GitHub) is federated inside Zitadel rather than configured directly on the gateway. Local email+password accounts are part of the design (decision #18/#19) but are not implemented yet — today, no OIDC config means no control plane.
Configuration
OIDC is configured entirely by environment variables, all-or-none for the three core values:
| Variable | Required | Description |
|---|---|---|
GIG_OIDC_ISSUER | yes* | Issuer URL, e.g. http://localhost:8082. Empty disables the control plane. |
GIG_OIDC_CLIENT_ID | yes* | OAuth client ID. |
GIG_OIDC_REDIRECT_URL | yes* | Callback URL, e.g. https://gig.example.com/api/auth/callback. Must be the browser-facing origin. |
GIG_OIDC_CLIENT_SECRET | no | Optional — empty for PKCE public clients. GIG_OIDC_CLIENT_SECRET_FILE is supported for Docker secrets. |
GIG_OIDC_ADMIN_ROLE | no | Zitadel project-role key that maps to the gateway admin role. Default gigmcp-admin. |
GIG_SESSION_TTL | no | Session lifetime as a Go duration. Default 168h. |
GIG_PUBLIC_URL | no | Browser-facing origin; an https:// prefix turns on Secure cookies. If unset with OIDC enabled, derived from the redirect URL. |
* set together or not at all — partial configuration is a startup error.
:::note When OIDC is unset
The /api control plane is disabled: requests get a descriptive 404 explaining which variables to set, and the dashboard shows a setup hint. The /mcp data plane keeps working with bearer tokens.
:::
The gateway runs OIDC discovery against the issuer at boot with a 10-second timeout and fails fast if it cannot complete — in the dev compose setup this is why Zitadel must be up first (see deployment).
Login flow
GET /api/auth/login— the gateway generates state, nonce, and a PKCE S256 challenge, stores them in a signed, httpOnly flow cookie (10-minute expiry, HMAC-SHA256 with a per-process random key), and 302s to the IdP's authorize endpoint.GET /api/auth/callback— verifies state, exchanges the code with the PKCE verifier, verifies the ID token (signature, issuer, audience, nonce), maps roles, and JIT-upserts the user by(issuer, subject).- The gateway issues an opaque session token (32 random bytes; only its SHA-256 is stored) in the httpOnly
gig_sessioncookie (SameSite=Lax,Secureiff the public URL is HTTPS) and redirects to/. POST /api/auth/logout— deletes the session row and clears the cookie. Local logout only; RP-initiated IdP logout is a follow-up.
Logins and logouts are recorded in the audit log.
The flow cookie's signing key is random per process: in-flight logins do not survive a gateway restart (users just retry). This assumes a single gateway instance — consistent with the SQLite single-listener architecture.
Roles
Two roles exist: admin and user. Role assignment is a pure function of the IdP: a user is admin iff their Zitadel project-roles claim (urn:zitadel:iam:org:project:roles) contains the configured GIG_OIDC_ADMIN_ROLE key. Roles are refreshed on every login — they are a cache of IdP state and are never edited locally. GET /api/users (admin-only) is accordingly read-only.
Admin-gated API operations:
POST /api/servers/install,DELETE /api/servers/{name}— server install/uninstallGET /api/users— user listPOST/DELETE /api/admin/impersonate— view-as impersonation
Admins also see all profiles and all audit events; regular users see only their own. Foreign profiles read as 404 to avoid leaking existence.
Impersonation (view-as)
Per design decision #20, impersonation is config-only view-as: an admin sees the target user's view (profiles, server status, audit) but cannot mutate anything or touch secrets.
- Time-boxed: the TTL is capped at 60 minutes (requested via
ttl_minutes; invalid or larger values clamp to the cap). - Read-only enforced by middleware: while impersonating, every non-GET/HEAD request returns 403 — the only allowed mutation is
DELETE /api/admin/impersonate(stop). Switching targets requires stopping first. - Loud: start and stop emit audit events naming both parties, attributed to the target so they appear in the impersonated user's own audit view.
- Visible:
GET /api/mereportsimpersonatingplus both identities, which drives the dashboard banner. - Admins cannot impersonate themselves, and admin role checks always use the real user, so a non-admin can never gain admin endpoints through impersonation.
Full impersonation (executing tools as the user) is a deliberate org-level toggle in the design, off by default and not currently implemented.
Token storage at a glance
| Token | Form at rest | Plaintext exposure |
|---|---|---|
| Session token | hex SHA-256 in the sessions table | httpOnly cookie only |
Profile token (gig_…) | hex SHA-256 in the profiles table | shown once at create/rotate |
Legacy GIG_BEARER_TOKEN | environment variable | operator-managed |
API credentials for sandboxed servers are a separate concern — they are envelope-encrypted in the vault and injected at the egress proxy, never handed to clients or sandboxes.