Skip to main content

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:

SurfaceEndpointsCredential
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:

VariableRequiredDescription
GIG_OIDC_ISSUERyes*Issuer URL, e.g. http://localhost:8082. Empty disables the control plane.
GIG_OIDC_CLIENT_IDyes*OAuth client ID.
GIG_OIDC_REDIRECT_URLyes*Callback URL, e.g. https://gig.example.com/api/auth/callback. Must be the browser-facing origin.
GIG_OIDC_CLIENT_SECRETnoOptional — empty for PKCE public clients. GIG_OIDC_CLIENT_SECRET_FILE is supported for Docker secrets.
GIG_OIDC_ADMIN_ROLEnoZitadel project-role key that maps to the gateway admin role. Default gigmcp-admin.
GIG_SESSION_TTLnoSession lifetime as a Go duration. Default 168h.
GIG_PUBLIC_URLnoBrowser-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

  1. 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.
  2. 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).
  3. The gateway issues an opaque session token (32 random bytes; only its SHA-256 is stored) in the httpOnly gig_session cookie (SameSite=Lax, Secure iff the public URL is HTTPS) and redirects to /.
  4. 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.

note

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/uninstall
  • GET /api/users — user list
  • POST / 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/me reports impersonating plus 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

TokenForm at restPlaintext exposure
Session tokenhex SHA-256 in the sessions tablehttpOnly cookie only
Profile token (gig_…)hex SHA-256 in the profiles tableshown once at create/rotate
Legacy GIG_BEARER_TOKENenvironment variableoperator-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.