Stage-batch14: add HERMES_WEBUI_PASSKEY feature flag for #2859 passkey support

Per the stage-batch14 ship plan, passkey/WebAuthn support is shipped
opt-in default-off behind an explicit feature flag so deployments can
disable the entire surface (UI + endpoints + credential storage) without
needing to delete code.

Enable via either:
  - HERMES_WEBUI_PASSKEY=1 environment variable, OR
  - webui_passkey_enabled: true in config.yaml

With the flag off:
  - are_passkeys_enabled() returns False even if credentials exist
  - is_auth_enabled() falls back to password-only checking
  - /login renders password-only (no passkey button)
  - All 6 /api/auth/passkey/* endpoints return 404 with a clear message
  - Settings → System → Passkeys section is hidden

Mirrors the #2527 notes-drawer flag shape (env-or-config, truthy parse).
Auth is high-stakes; opt-in lets us land the code while keeping default
deployments on the well-tested password-only path.

Touches: api/auth.py (new _passkey_feature_flag_enabled helper, gated
are_passkeys_enabled), api/routes.py (6 endpoint guards).
This commit is contained in:
nesquena-hermes
2026-05-25 00:16:12 +00:00
parent 1b48643f63
commit 46ed70bfde
2 changed files with 57 additions and 4 deletions

View File

@@ -296,8 +296,42 @@ def is_password_auth_enabled() -> bool:
return get_password_hash() is not None
def _passkey_feature_flag_enabled() -> bool:
"""Return True if the passkey/WebAuthn surface is enabled for this deployment.
Passkey support is opt-in default-off behind a feature flag so deployments
that don't want the WebAuthn surface (or whose RP-ID setup isn't ready for
non-localhost hosts) can disable it entirely with no UI surface, no
endpoints, no credential storage. To enable:
- Set ``HERMES_WEBUI_PASSKEY=1`` in the environment, OR
- Set ``webui_passkey_enabled: true`` in the per-profile config.yaml
With the flag off, ``are_passkeys_enabled()`` always returns False even if
credentials were registered in the past, and ``/login`` shows password-only.
"""
env_value = os.getenv("HERMES_WEBUI_PASSKEY", "")
if env_value:
return env_value.strip().lower() in {"1", "true", "yes", "on"}
try:
from api.config import get_config
cfg = get_config()
if isinstance(cfg, dict):
raw = cfg.get("webui_passkey_enabled")
if isinstance(raw, bool):
return raw
if isinstance(raw, str):
return raw.strip().lower() in {"1", "true", "yes", "on"}
except Exception:
pass
return False
def are_passkeys_enabled() -> bool:
"""True if at least one local passkey credential is registered."""
"""True if the passkey feature flag is on AND at least one local passkey credential is registered."""
if not _passkey_feature_flag_enabled():
return False
try:
from api.passkeys import passkeys_available