Authentication & sessions
Four independent authentication mechanisms, all converging on the same permission-checking identity.
KHUB supports three independent authentication mechanisms, plus enterprise SSO, all converging on the same AuthenticatedUser principal used by every permission check.
1. Browser sessions (cookie-based JWT)#
Logging in via the web UI issues two httpOnly, SameSite=Lax cookies, scoped to / and marked Secure whenever the request arrived over TLS:
| Cookie | Lifetime | Purpose |
|---|---|---|
khub_access | 15 minutes (JWT_ACCESS_TTL) | A signed JWT carrying the user ID, read on every request by a Passport JWT strategy that extracts it from the cookie (not an Authorization header). |
khub_refresh | 30 days (JWT_REFRESH_TTL) | A signed JWT tied to a persisted, revocable session row. POST /api/auth/refresh exchanges it for a new access/refresh pair (rotation on every use); the web client calls this transparently on a 401. |
khub_impersonator | Session-admin-only | Set only while a site admin is impersonating another user (see Instance administration); holds the admin's own identity separately from the impersonated session's cookies so the impersonation can be ended and audited correctly. |
Refresh tokens are individually revocable — Settings → Security → Sessions lists every active session (by refresh-token identity) and can revoke any of them remotely, which invalidates that khub_refresh cookie's ability to mint new access tokens without affecting other sessions.
Two-factor authentication (TOTP) inserts a short-lived (5 minute), single-purpose intermediate JWT between password verification and issuing real session cookies — the intermediate token authorizes nothing except completing the second factor.
2. Personal access tokens#
For git over HTTPS, the npm/OCI registries, and direct API/MCP access. Created under Settings → Access tokens:
- Format:
khub_pat_followed by 64 hex characters (32 random bytes). The raw value is shown exactly once at creation and is not recoverable — only a SHA-256 hash and a 12-character prefix (for identifying a token in a list) are persisted. - Scopes:
repo:read,repo:write,api— the token carries these explicitly rather than inheriting the user's full permission set; a leakedrepo:readtoken cannot push code or call non-Git API endpoints. - Creating a token with the
apiscope requires re-entering your account password in the same request, since that scope grants the broadest surface (including anything reachable via MCP). - Optional expiry date; expired tokens are rejected at validation time, not swept — there is nothing to "clean up" for security purposes.
Use it as the password half of HTTP basic auth for Git (https://<username>:<token>@host/...), as the OCI/npm registry password, or as a Bearer token against the REST API and MCP bridge.
3. SSH keys#
Add an Ed25519 or RSA public key under Settings → SSH keys. services/ssh-git authenticates incoming connections against these keys and maps a successful auth to the same user/permission resolution as everything else — there is no separate SSH-specific ACL.
4. Enterprise SSO (SAML 2.0)#
An organization can configure a SAML identity provider under Organization settings → Security. The flow is standard SP-initiated SAML:
- 1The user is redirected to
GET /api/auth/saml/:orgSlug/login, which redirects to the configured IdP's SSO URL with a signedAuthnRequest. - 2The IdP redirects the browser back to the Assertion Consumer Service endpoint,
POST /api/auth/saml/:orgSlug/acs, with a signed SAML Response. - 3KHUB verifies the assertion's signature against the IdP's configured certificate, provisions or matches the user by the assertion's email/NameID, and issues normal
khub_access/khub_refreshcookies — from that point on, a SAML-authenticated session is indistinguishable from a password-authenticated one.
The ACS endpoint is deliberately exempted from KHUB's CSRF origin check: the request arrives as a same-origin-looking POST from the IdP's domain, which a strict origin check would reject, but the SAML assertion's own cryptographic signature — not the browser's cookie/origin context — is the actual security boundary for that endpoint.
Request-level identity resolution#
Regardless of which mechanism authenticated the request, every downstream handler receives the same shape: { id, username, isAdmin, impersonatedBy? }. Authorization checks (see Authorization & permissions) never branch on how the request was authenticated — a personal access token with the api scope and a browser session belonging to the same user can do exactly the same things.
