UI and coordinator authentication¶
This document explains which authentication modes the Homer coordinator exposes to the web UI and API clients, how operators switch between local (internal), LDAP, and OAuth2, and how tokens (JWT sessions vs static API tokens) are used.
For full LDAP and OAuth2 configuration field tables and examples, see AUTH_LDAP_AND_OAUTH.md (canonical). OpenAPI details live in src/coordinator/docs/openapi.yaml.
Architecture¶
- The coordinator is the authority: it serves
GET /api/v4/auth/providers, creates sessions, validates requests, and issues JWTs. - The bundled UI (
src/ui) calls the v4 API underimport.meta.env.VITE_API_BASEor the default/api/v4. On login the coordinator sets an HttpOnly session cookiehomer_session(Path/api/v4,SameSite=Laxby default) so the session is shared across browser tabs without putting the JWT in JavaScript. The UI sendscredentials: includeon API calls. Optional Remember me stores the JWT inlocalStorage(homer_v4_token) for Bearer/WebSocket fallback — less safe on shared machines. Seesrc/ui/src/lib/authTokenStorage.ts.
There is no separate “auth mode” flag in the UI alone: available methods are entirely determined by coordinator configuration after restart.
Authentication types¶
| Mode | Coordinator | Password / flow | Typical POST /api/v4/auth/sessions body |
|---|---|---|---|
| Local (internal) | Enabled unless coordinator.auth.disable_password_login is true |
Users in the coordinator settings DuckDB (users table). Credentials are checked only against users (no config-only login). Recommended: "coordinator.auth": {"type":"internal"} (or omit auth / string "internal") — first startup inserts admin once if no row exists for that username. When admin_password_hash is omitted, a random password is logged once at coordinator startup; otherwise use your configured hash or wizard-generated bcrypt. Explicit admin_user / admin_password_hash in JSON or env supports --reset-admin-password (see AUTH_LDAP_AND_OAUTH.md). |
{"username":"…","password":"…"} or "type":"internal" (default). |
| LDAP | Advertised only if coordinator.ldap.enable is true and coordinator.ldap.host is non-empty |
Directory bind + optional group rules for admin vs user. | {"username":"…","password":"…","type":"ldap"}. |
| OAuth2 | Optional; at most one provider from coordinator.oauth2_provider |
Authorization code on the coordinator: GET …/redirect builds the IdP authorize URL, IdP returns code to …/callback, coordinator exchanges code + loads profile, then SPA exchanges the one-time query token for a JWT (see below). |
No password session; use OAuth routes. |
Notes:
- Internal and LDAP are both “password backends”. The API accepts
typeofinternalorldaponly; anything else is400. - OAuth2 is orthogonal: it does not use
typeonPOST /auth/sessions; it usesGET /api/v4/auth/oauth2/{provider}/redirect,GET /api/v4/auth/oauth2/{provider}/callback, andPOST /api/v4/auth/oauth2/token.
Discovery: what the UI shows¶
GET /api/v4/auth/providers (no authentication) returns data.internal, data.ldap, and data.oauth2 (array with zero or one enabled row).
The UI (src/ui/src/loginProviders.ts, LoginPage.tsx):
- Builds the list of enabled password methods from
internalandldaprows (enable: true). - If more than one password method is enabled, it shows an “Authentication” dropdown so the user picks internal vs ldap; the selected value is sent as JSON
typeonPOST /api/v4/auth/sessions. - If only one password method is enabled,
typeis forced to that method’stype(no dropdown). - For OAuth2, it shows a “Continue with …” control per enabled provider name and navigates the browser to
{apiBase}/auth/oauth2/{name}/redirect. - If the provider has
auto_redirect: true, the UI immediately starts that redirect once on the login page.
Switching between local and LDAP for end users: use the login dropdown when both are enabled.
Switching at deployment level (operators):
- Local only: set
"coordinator.auth": {"type":"internal"}(recommended), omitauth(defaults to internal), or legacy string"internal"; leave LDAP disabled or omit host; remove or disableoauth2_provider. - Local + LDAP: set
coordinator.ldapwithenable: trueand a non-emptyhost(see canonical doc). - Add OAuth2: set
coordinator.oauth2_providerwith authorization code fields (client_id,auth_url,token_url,redirect_url,profile_url, …); restart coordinator. - OAuth-only (no password form): set
coordinator.auth.disable_password_login: trueand configureoauth2_provider. Optionalauto_redirect: truesends users to the IdP immediately. - Force OAuth-first UX (password still available): set
auto_redirect: trueon the single OAuth provider while keeping password backends enabled for break-glass accounts.
OAuth2 flow and JWT¶
GET /api/v4/auth/oauth2/{provider}/redirect— Coordinator builds the IdP authorize URL (OAuth2state+ optional PKCE), then HTTP 302 to the IdP.GET /api/v4/auth/oauth2/{provider}/callback— IdP returnscode; coordinator validatesstate, exchangescodefor tokens attoken_url, loads the user fromprofile_url, maps or creates a DuckDBusersrow, then issues a short-lived one-time token and redirects the browser tocallback_urlwith?token=<one-time>(or?oauth_error=on failure). Redirect targets are validated: configurecallback_urlon the provider; user-supplied absoluteredirect_urivalues are not accepted unless they match that origin (see AUTH_LDAP_AND_OAUTH.md — Redirect URL safety).POST /api/v4/auth/oauth2/tokenwith body{"token":"<one-time>"}— Consumes the one-time token once and returnsdata.tokenas the JWT session (same shape as password login).
The bundled UI (src/ui/src/App.tsx) reads oauth_error from the query (toast) and performs the one-time → JWT POST when token is present (no Authorization header on that POST). Custom SPAs must do the same.
API clients and SPAs should exchange the query token for the JWT and then send Authorization: Bearer <jwt> on subsequent calls. One-time tokens are not JWTs and will not validate as Bearer credentials (sending them as Bearer causes 401 on /me and other protected routes).
Multi-instance warning: OAuth state, PKCE verifiers, and one-time tokens are stored in-process; multiple coordinator replicas need a shared store for this flow to be reliable (see canonical doc).
JWT session tokens¶
- Login response:
POST /api/v4/auth/sessions(password) andPOST /api/v4/auth/oauth2/token(after exchange) returndata.token(JWT) anddata.scope/data.user.admin, and (whencoordinator.jwt.cookie_enableis true, the default) setSet-Cookie: homer_session=<jwt>; HttpOnly; …. - Usage (pick one per request):
- Browser UI (recommended): HttpOnly cookie — automatic on same-origin
/api/v4withcredentials: include; not readable from JS (mitigates XSS token theft). - Scripts / API clients:
Authorization: Bearer <jwt>(unchanged). - WebSocket: cookie on the handshake when using the UI; otherwise
?access_token=<jwt>(browsers cannot setAuthorizationon WS upgrade). - Lifetime:
coordinator.jwt.expire_hours(default 24) andcoordinator.jwt.secret. Ifsecretis empty in config, Homer generates and persists/.homer_jwt_secretbesidesettings_db_pathat startup (SECURITY.md). - Cookie settings (
coordinator.jwt):cookie_enable(default true),cookie_name(defaulthomer_session),cookie_same_site(Lax|Strict|None),cookie_secure(optional; auto from TLS /X-Forwarded-Proto). - CSRF: Cookie-authenticated POST/PUT/PATCH/DELETE requests validate
Origin/Refereragainst the request host (defense in depth withSameSite=Lax). - Logout / revocation: JWT
jtiis the session id.DELETE /api/v4/auth/sessions/currentrevokes the caller’s session and clears the cookie (bundled UI logout).DELETE /api/v4/auth/sessions/{sessionId}revokes a specificjtiwhen it matches the Bearer/cookie session.
Why not sessionStorage only?¶
Homer 11.0.229+ briefly stored the JWT in sessionStorage (tab-scoped) after a CodeQL security pass. That meant each new tab required login — painful for search deep links opened in another tab. HttpOnly cookies restore multi-tab UX without exposing the JWT to JavaScript like always-on localStorage did in Homer 7.
Remember me (optional, UI)¶
Login body may include "remember": true. The UI then keeps data.token in localStorage so Bearer headers and WebSocket access_token work without relying on the cookie alone. Skip on shared workstations.
Static API tokens (Auth-Token)¶
Separate from JWT login, the coordinator can accept static tokens stored in the settings DuckDB auth_token table when API token access is enabled.
Coordinator configuration (coordinator.api_settings):
| Field | Purpose |
|---|---|
enable_token_access |
If true, the JWT middleware first checks the configured header for a raw token matching an auth_token row. |
auth_token_header |
Header name (default Auth-Token). |
When a valid row is found, the request is treated as authenticated with a synthetic user derived from the row’s user_object JSON: username and admin if user_group equals admin (case-insensitive) — see authenticateWithAuthTokenHeader in src/coordinator/handlers/auth.go.
How to use:
- Enable
enable_token_accessand restart. - Create a token (admin UI Settings → Auth tokens or the v4 CRUD API under
/api/v4/auth-tokens). - Call APIs with
Auth-Token: <secret>(or your custom header name) instead ofAuthorization: Bearer …, or rely on middleware order: if token access is enabled, that header is tried before Bearer.
Tokens support optional expiry, call limits, and active flag; the server increments usage on successful lookups.
Summary table¶
| Goal | Mechanism |
|---|---|
| Local users | DuckDB users + POST /auth/sessions with type omitted or internal. Prefer {"type":"internal"} / omitted auth / string "internal" for bootstrap; use --reset-admin-password with admin_password_hash in config (or env) for recovery — AUTH_LDAP_AND_OAUTH.md. |
| LDAP users | Configure LDAP; POST /auth/sessions with "type":"ldap". |
| OAuth2 users | Configure single oauth2_provider; redirect + POST /auth/oauth2/token. |
| Scripts / integrations without JWT | Enable api_settings.enable_token_access; send Auth-Token. |
| Revoke JWT session (UI logout) | DELETE /api/v4/auth/sessions/current. |
| Revoke specific session | DELETE /api/v4/auth/sessions/{jti}. |
See also¶
- AUTH_LDAP_AND_OAUTH.md — LDAP and OAuth2 configuration, examples, operational notes.
- AUTH_OAUTH.md — pointer to the canonical LDAP+OAuth doc.
src/coordinator/handlers/auth.go,auth_v4.go— session creation, JWT middleware, Auth-Token path.src/ui/src/LoginPage.tsx,loginProviders.ts— UI discovery and passwordtypebehaviour.