github →

New request

#1576: feat(auth): multi-user accounts + 2FA / passkeys / external IdP — auth hardening umbrella

We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.

Issue
enhancementauthtracking

Summary

The WebUI currently ships with a single shared password as its only auth mechanism (HERMES_WEBUI_PASSWORD or settings.json[password_hash], validated in api/auth.py). Multiple self-hosted users have asked for stronger options:

  • RoboRiley (Discord, May 03 2026) running behind a VPN + reverse proxy:

    "single password feels a little weak so I put another security layer at the VPN. But it would be nice to be able to lock down the app with a user id in addition to the password and token based 2FA."

  • spoder (Discord, same thread):

    "For more than basic auth mechanisms you probably want to use something like Authentik"

These are two different requests pointing at the same gap. Self-hosters who expose the WebUI to the open internet (or even to a private network with multiple humans) need a defensible answer.

Three layers, increasing scope

This is intentionally framed as one umbrella with three layers so we can ship incrementally rather than block on the full pipe. Each layer should be a separate sub-issue once we agree on direction.

Layer 1 — Username + password (small, ships first)

  • Add an optional users table in settings.json (or a sibling users.json) with {username, password_hash, created_at, last_login_at}.
  • Backwards compatible: when the users table is empty, fall back to the existing single-password flow (HERMES_WEBUI_PASSWORD / settings.json[password_hash]) so nothing breaks for current installs.
  • Login page gains a username field that's hidden by default and only shown when more than one user exists OR an admin has explicitly enabled multi-user mode (Settings → System → "Enable multi-user accounts").
  • Session cookie now embeds user_id, used by the audit log and any future per-user features.
  • New env var HERMES_WEBUI_USERS_FILE for ops who want to manage users via config file vs UI.
  • Why this first: addresses RoboRiley's "user id in addition to the password" directly with the smallest possible diff, keeps the door open for layers 2-3.

Layer 2 — Token-based 2FA (TOTP) and/or passkeys (WebAuthn)

  • TOTP: standard rfc6238, 6-digit codes, encrypted secret stored per-user. QR code enrollment in Settings → My Account. Recovery codes (10 single-use) generated at enrollment and shown once.
  • Passkeys: WebAuthn registration + authentication. One or more authenticators per user. Cross-platform via the standard browser API.
  • 2FA enforcement is per-user (user can opt in) OR per-instance (admin can require for all). Default off for backwards compat.
  • Failed-attempt rate limiting + lockout (already partially present for the single-password flow, generalize to per-user).
  • Why second: this is the one RoboRiley explicitly asked for, but it depends on Layer 1 because TOTP secrets / passkey credentials need to attach to a specific user.

Layer 3 — External IdP via OIDC / SAML / oauth-proxy (advanced)

This is what spoder pointed at. Two viable approaches:

3a. Native OIDC support inside the WebUI. Configure an OIDC issuer URL + client_id + client_secret in settings.json. Standard PKCE flow. JIT user provisioning (first login creates the local user). Group claim → admin/user role mapping.

3b. Trust a reverse-proxy header. Cleaner for deployments already running oauth2-proxy / Authentik / Authelia / tinyauth in front of the WebUI. New env var pair: HERMES_WEBUI_TRUSTED_PROXY_AUTH_HEADER (e.g. X-Authentik-Username) and HERMES_WEBUI_TRUSTED_PROXY_NETS (CIDR allowlist for which upstreams may set this header — critical, otherwise it's a header-spoof RCE). When a request arrives from a trusted proxy IP with the header set, skip our login flow and treat the user as authenticated as that username.

3b is much smaller code surface and probably the right starting point. Most self-hosters running Authentik / authelia don't want a second login page anyway. 3a can come later if the demand is there.

Out of scope (do not pull in)

  • Role-based access control beyond admin/user binary
  • SCIM provisioning
  • Audit-log shipping to external SIEMs
  • Hardware token (YubiKey) flow separate from WebAuthn passkeys (passkeys cover this)
  • Per-session permission scopes
  • Anything that requires a database migration beyond settings.json shape changes (we ship as a single Python script — keep it that way)

These are all reasonable extensions but each is an issue's worth of design on its own. Cut them now to keep the umbrella shippable.

Compatibility constraints

  • Single-user installs must keep working unchanged. This is the dominant deployment shape. If users.table is empty, HERMES_WEBUI_PASSWORD env var or settings.json[password_hash] continues to work.
  • HERMES_WEBUI_PASSWORD env-var precedence stays as documented in #1560. If the env var is set, the API still refuses settings-side password changes; in multi-user mode the env var becomes the password for an implicit admin user.
  • No new heavy dependencies. TOTP needs only pyotp (~12kb). WebAuthn needs webauthn library (~80kb, well-maintained, no transitive deps that are problematic). OIDC needs nothing beyond the stdlib if we hand-roll the PKCE flow against a documented issuer.
  • Storage stays on disk in settings.json / users.json for portability. No SQLite, no Postgres. We're a portable Python script.

What I want before we build

Please react / comment with the auth model that matches your deployment so we know what to ship first:

  • 👍 Just give me usernames and per-user passwords (Layer 1)
  • 🚀 I need TOTP / passkeys (Layer 2)
  • 🛡️ I'm running behind Authentik / oauth2-proxy / Authelia and just want header-trust (Layer 3b)
  • 🔑 I want full native OIDC inside the WebUI (Layer 3a)

For self-hosters, please also share:

  • Whether the WebUI is exposed to the public internet, a private VPN, a LAN, or only localhost
  • How many human users actually share the install
  • Whether you already run an IdP and which one

This is a sizable feature; getting the demand signal right matters more than which auth library we pick.

Reporters

  • RoboRiley (Discord, #report-bugs, May 03 2026 15:12 PT) — explicit ask for "user id in addition to the password and token based 2FA"
  • spoder (Discord, same thread, 15:26 PT) — pointed at Authentik for "more than basic auth mechanisms"
  • Relayed by AvidFuturist

Related

  • #1560 — password env-var lock UX (recently shipped — established the env-var-precedence pattern this issue extends)
  • #1139 — original umbrella that #1560 split out from
Assessmentadvisory
feature●●● hard80% confidence

Multi-layer authentication umbrella covering user tables, optional 2FA, and external IdP integration spans the auth backend, onboarding, and frontend login flows.

Likely files
  • api/auth.py
  • api/onboarding.py
  • static/login.js
  • api/users.py
  • settings.json
  • tests/auth_regression.py
Create the request

This opens a fresh agent run and a draft PR for issue #1576.

Cancel