Hermes WebUI Authentication Methods: Password, Passkeys, and HMAC Cookies Explained

Hermes WebUI supports three authentication mechanisms: PBKDF2-SHA256 password hashing, optional WebAuthn passkeys, and HMAC-signed session cookies that maintain state after initial login.

The nesquena/hermes-webui repository implements a flexible authentication layer that protects both the web interface and API endpoints. Understanding these Hermes WebUI authentication methods is essential for securing deployments in production environments. The system treats passwords, passkeys, and session tokens as complementary components of a unified security model.

Understanding Hermes WebUI Authentication Architecture

The authentication system centers on api/auth.py, which coordinates multiple verification strategies through a common interface.

Auth Enablement Logic

The is_auth_enabled() function returns True when either password authentication or passkey support is active. As implemented in api/auth.py lines 53-55, this check gates access to protected resources by verifying is_password_auth_enabled() OR are_passkeys_enabled().

Password hashes resolve through get_password_hash() (api/auth.py lines 92-97), which prioritizes the HERMES_WEBUI_PASSWORD environment variable before falling back to settings.json. The implementation caches PBKDF2-SHA256 computations to avoid the approximately one-second hashing cost on every request.

Session Management Overview

After successful authentication via any method, create_session() generates a 64-character hex token, attaches an expiry timestamp, and signs the payload using _signing_key() (api/auth.py lines 89-97). The set_auth_cookie() function (api/auth.py lines 72-82) then transmits this as an HttpOnly, SameSite-Lax cookie named hermes_session.

Password Authentication

Password authentication uses PBKDF2-SHA256 hashing with 600,000 iterations. The system stores only the hash, never the plaintext password.

Configuration Methods

You can configure password authentication via environment variable or configuration file:


# Environment variable (recommended for containerized deployments)

export HERMES_WEBUI_PASSWORD='MyStrongPassword'

# Or generate a PBKDF2 hash for settings.json

python -c "import hashlib,os,base64; print(hashlib.pbkdf2_hmac('sha256','MyStrongPassword'.encode(),os.urandom(32),600_000).hex())"

The _resolve_session_ttl() and get_password_hash() functions (api/auth.py lines 29-34 and 92-97) handle this resolution, reading the environment first and caching results after the initial computation.

API Login Endpoint

Authenticate programmatically to receive the HMAC-signed session cookie:

curl -X POST -H "Content-Type: application/json" \
     -d '{"password":"MyStrongPassword"}' \
     http://localhost:8789/api/auth/login -c cookies.txt

This request triggers check_auth() in api/auth.py lines 89-101, which validates the PBKDF2 hash and establishes the session.

Passkey (WebAuthn) Authentication

Passkey support provides phishing-resistant authentication using hardware or platform authenticators. This feature requires explicit opt-in through a feature flag.

Feature Flag and Availability

The _passkey_feature_flag_enabled() function (api/auth.py lines 108-119) checks for HERMES_WEBUI_PASSKEY=1 or the webui_passkey_enabled configuration key. However, the flag alone does not enable passkey login—are_passkeys_enabled() (api/auth.py lines 140-147) also verifies that at least one credential exists in passkeys.json.

Enable passkey support globally or per-profile:


# Global environment variable

export HERMES_WEBUI_PASSKEY=1

# Or in config.yaml

webui_passkey_enabled: true

Registration Flow

The api/passkeys.py file handles WebAuthn ceremonies. Registration begins with a challenge from /api/auth/passkey/options and concludes with finish_registration() storing the credential in passkeys.json (lines 102-104).

// Frontend registration example
fetch('/api/auth/passkey/options', {method: 'POST'})
  .then(r => r.json())
  .then(opts => navigator.credentials.create({publicKey: opts}))
  .then(cred => fetch('/api/auth/passkey/register', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({response: cred, label: 'My-YubiKey'})
      }));

Passkey Login

Existing passkey users authenticate through the WebAuthn get ceremony:

fetch('/api/auth/passkey/options', {method: 'POST'})
  .then(r => r.json())
  .then(opts => navigator.credentials.get({publicKey: opts}))
  .then(assertion => fetch('/api/auth/passkey/login', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({response: assertion})
      }));

Success returns the same HMAC-signed hermes_session cookie used for password logins.

HMAC-Signed Session Cookies

The session cookie binds the authentication state to subsequent requests without requiring repeated password entry or passkey verification.

The set_auth_cookie() function (api/auth.py lines 72-82) configures the cookie with:

  • Name: hermes_session
  • HttpOnly: Prevents JavaScript access
  • SameSite: Lax
  • Secure: Enabled when appropriate
  • Max-Age: Derived from _resolve_session_ttl()

Session Verification

Each protected request invokes verify_session() (api/auth.py lines 110-123), which:

  1. Validates the HMAC signature against the server-side key
  2. Prunes expired entries from the session store
  3. Confirms the token exists and is active

If verification fails, check_auth() returns HTTP 401 for API calls or redirects to the login page for UI routes.

Using Session Cookies

After authentication, include the cookie in subsequent API requests:

curl -b cookies.txt http://localhost:8789/api/agents

The server validates this cookie through verify_session() before serving protected content.

Summary

  • Password Authentication: Uses PBKDF2-SHA256 hashing via HERMES_WEBUI_PASSWORD env var or settings.json, implemented in api/auth.py lines 92-97.
  • Passkey Authentication: WebAuthn support controlled by HERMES_WEBUI_PASSKEY flag and credential existence checks in api/passkeys.py and api/auth.py lines 108-147.
  • Session Management: HMAC-signed cookies (hermes_session) created by create_session() and verified by verify_session() in api/auth.py, with HttpOnly and SameSite-Lax security attributes.
  • Unified Protection: The is_auth_enabled() check in api/auth.py coordinates all methods, while check_auth() gates every request.

Frequently Asked Questions

How do I enable password-only authentication without passkeys?

Set the HERMES_WEBUI_PASSWORD environment variable or add a PBKDF2-SHA256 hash to settings.json. Do not set HERMES_WEBUI_PASSKEY=1. The is_auth_enabled() function will return True based on is_password_auth_enabled() while are_passkeys_enabled() remains False.

What happens if I enable passkeys but have no registered credentials?

The passkey authentication option remains unavailable. The are_passkeys_enabled() function (api/auth.py lines 140-147) requires both the feature flag (_passkey_feature_flag_enabled()) AND at least one credential stored in passkeys.json. Users must first register a passkey through the registration flow before the login option appears.

Are session cookies vulnerable to tampering?

No. The hermes_session cookie contains an HMAC signature generated by _signing_key() and verified by verify_session() (api/auth.py lines 110-123). Without the server-side signing key, clients cannot forge valid session tokens. The cookie also uses HttpOnly and Secure flags to prevent XSS and interception attacks.

Can I use password and passkey authentication simultaneously?

Yes. The authentication system supports both methods concurrently. is_auth_enabled() activates when either method is configured, and users can choose their preferred authentication mechanism at login. Both successful password logins and successful passkey assertions receive identical HMAC-signed session cookies for subsequent request authorization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →