feat(dashboard-auth): add BasicAuthProvider username/password plugin

A bundled, zero-infrastructure 'just put a password on my dashboard'
provider that uses the supports_password extension point. No external IDP,
no database: sessions are stateless HMAC-signed tokens the provider mints
and verifies itself, and passwords are hashed with stdlib scrypt (no
third-party dependency — deliberately avoids bcrypt to keep the dep
surface unchanged).

  - plugins/dashboard_auth/basic: BasicAuthProvider (scrypt verify with a
    constant-time dummy-hash path for unknown users so the endpoint is not
    a username-timing oracle; access/refresh tokens carry a 'kind' claim
    that verify/refresh enforce; cross-secret tokens are rejected). The
    register() entry point mirrors the Nous plugin's config/env precedence
    (env wins; empty treated as unset) and LAST_SKIP_REASON channel.
  - config.py: document the canonical dashboard.basic_auth.* surface
    (username / password_hash / password / secret / session_ttl_seconds).

Activates only when username + (password or password_hash) are set, so
OAuth users and loopback/--insecure operators are unaffected. Without an
explicit secret a random per-process key is generated (logged): fine for a
single process, but sessions then don't survive restart or span workers.
This commit is contained in:
Ben
2026-06-04 01:02:25 -07:00
committed by Teknium
parent ed9e8ba097
commit acb0e2bacb
3 changed files with 526 additions and 0 deletions
+28
View File
@@ -1482,6 +1482,34 @@ DEFAULT_CONFIG = {
"client_id": "", # agent:{instance_id} — Portal provisions this
"portal_url": "", # blank → use plugin default (production Portal)
},
# Username/password gate configuration — read by the bundled
# ``dashboard_auth/basic`` plugin (a self-hosted "just put a
# password on my dashboard" provider that needs no OAuth IDP).
# The plugin registers a password provider when ``username`` plus
# either ``password_hash`` (preferred — no plaintext at rest) or
# ``password`` (plaintext, hashed in-memory at load) are set. Each
# key is overridable by an env var
# (``HERMES_DASHBOARD_BASIC_AUTH_USERNAME`` /
# ``_PASSWORD_HASH`` / ``_PASSWORD`` / ``_SECRET`` /
# ``_TTL_SECONDS``), env winning when non-empty. Leave ``username``
# empty (the default) to keep the plugin a no-op — loopback /
# ``--insecure`` operators and OAuth users are unaffected.
#
# ``secret`` is the HMAC key used to sign the stateless session
# tokens this provider mints. When empty, a random per-process key
# is generated — fine for a single process, but sessions then
# don't survive a restart or span multiple workers. Set an
# explicit ``secret`` (32+ random bytes, base64/hex/raw) for
# stable multi-worker / restart-surviving sessions. Compute a
# ``password_hash`` with
# ``python -c "from plugins.dashboard_auth.basic import hash_password; print(hash_password('PW'))"``.
"basic_auth": {
"username": "", # blank → plugin no-op (no password provider)
"password_hash": "", # scrypt$... (preferred — no plaintext at rest)
"password": "", # plaintext fallback (hashed in-memory at load)
"secret": "", # token-signing key; blank → random per-process
"session_ttl_seconds": 0, # 0 → plugin default (12h)
},
# Public URL override (env: ``HERMES_DASHBOARD_PUBLIC_URL``).
# When set, this is the complete authority — scheme + host +
# optional path prefix (e.g. ``https://example.com/hermes``) —