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 makes that cookie persistent (Max-Age=jwt.expire_hours); without it the cookie is session-scoped. The JWT is not stored inlocalStorage. 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; API clients may pass
?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 coordinator then sets a persistent HttpOnly cookie (Max-Age = jwt.expire_hours). Without remember, the cookie is session-scoped (cleared when the browser closes). The bundled UI never writes the JWT to localStorage.
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.
OAuth-only: CLI / scripts without password login¶
coordinator.auth.disable_password_login: true hides the username/password form and rejects POST /api/v4/auth/sessions with 403. Username/password API login (including homer search --user/--pass) does not work in that mode.
Use an admin Auth-Token for automation instead. Alias CRUD (POST/PUT/DELETE /api/v4/aliases) requires admin: create the token while signed in as an admin user so user_object.user_group is admin.
- Set
coordinator.api_settings.enable_token_accesstotrueand restart. - Sign in once via OAuth as an admin (Settings → Auth tokens).
- Call the API with
Auth-Token: <secret>(notAuthorization: Bearer):
# list
curl -sS -H "Auth-Token: <secret>" http://127.0.0.1:8080/api/v4/aliases
# create
curl -sS -H "Auth-Token: <secret>" -H "Content-Type: application/json" \
-X POST http://127.0.0.1:8080/api/v4/aliases \
-d '{"alias":"sbc-1","ip":"10.0.0.1","port":0,"mask":32,"status":true}'
# update (guid from the list/create response)
curl -sS -H "Auth-Token: <secret>" -H "Content-Type: application/json" \
-X PUT http://127.0.0.1:8080/api/v4/aliases/<guid> \
-d '{"alias":"sbc-1","ip":"10.0.0.1","port":0,"mask":32,"status":true}'
# delete
curl -sS -H "Auth-Token: <secret>" \
-X DELETE http://127.0.0.1:8080/api/v4/aliases/<guid>
The same token works with homer search --token <secret>. There is no dedicated homer alias subcommand.
If the UI is unreachable even via OAuth, temporarily set disable_password_login to false, create the token, then turn OAuth-only back on. Direct writes into the settings DuckDB alias table are possible but not recommended while Homer is running (file lock + in-memory alias cache).
Alias endpoints are listed in COORDINATOR.md. OAuth-only config: AUTH_LDAP_AND_OAUTH.md.
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. |
| OAuth-only + alias CLI | Admin Auth-Token + POST/PUT/DELETE /api/v4/aliases (details). |
| 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.