eetr-auth
Features

Authentication

Password authentication with Argon2id, password policies, passkeys (WebAuthn), and multi-factor authentication.

The sign-in experience supports passwords and passkeys, with MFA layered on top.

eetr-auth sign-in page with username and password fields and a 'Sign in with passkey' button
The sign-in page — password or passkey, branded with your site title and logo.

Password authentication

  • Passwords are hashed with Argon2id via the argon-hasher Cloudflare Worker.
  • Configurable fallback hash method via the HASH_METHOD environment variable (argon by default; md5 for local development).
  • Password reset via a time-limited JWT token sent to email — single-use and DB-backed. Requesting a new reset purges earlier pending links, and any password change (admin, self-service, or another reset) invalidates outstanding reset links.

Password policies

Per-environment password policies are managed from Setup → Password policies.

  • A policy is a named rule set: an enabled flag, complexity rules (min length, optional max length, a minimum count of uppercase / lowercase / number / special characters — 0 = not required, so a value of 2 demands at least two of that class — and "must not contain the username or email local-part"), and a max password age (days; 0 = never expires).
  • A policy can be assigned to one or more environments, but each environment holds at most one policy (a UNIQUE constraint enforces this).
  • Admins don't belong to an environment, so a single admin sign-in policy is selected globally and stored on site_settings (admin_password_policy_id). None enforces no policy for admins.

Max-age enforcement at login

When a user signs in, the applicable max age is applied — for admins, the admin sign-in policy; for everyone else, the strictest enabled max age across the environments they are granted. If their password is older, sign-in is halted before MFA and a reset is forced (a reset link is emailed when delivery is configured, otherwise the user is directed to an administrator). Users whose password change predates this feature are never expired until their next change.

Setup → Password policies editor with min length, character-class minimums, max age, and per-environment assignment
Setup → Password policies — complexity rules, max age, and per-environment assignment (plus the global admin sign-in policy).

Passwordless test sign-in

When the authorizing client is a test client, the sign-in page renders a picker of the test users granted that client's environment instead of the password form. One click signs in, the consent screen is skipped for that test user, and the browser lands back at the client's redirect_uri with an authorization code.

The picker replaces the password form only on the sign-in redirect GET /authorize issues, which is marked by callbackUrl=/oauth/confirm. Browsing to the sign-in page gets the password form, even if a test client's oauth_pending cookie from a few minutes ago is still valid. That marker is in the URL and so is the visitor's to set, but it grants nothing on its own: the test-user provider re-derives the live pending authorization, the client, and the user server-side before it mints a session. For the same reason a real user reaching a test client is shown the consent screen rather than being passed straight through: it is the only step that offers "sign in with a different account".

Which client is authorizing is read from the signed, httpOnly oauth_pending cookie set by GET /authorize — the page is reached by redirect, so there is nothing in the URL to read. That cookie lives 300 seconds; if it lapses while the picker sits open, the page says the request expired rather than silently falling back to a password form.

The provider that mints the session takes only a user id, and is directly reachable, so the id is treated as a selector and never as authority. Every condition is re-derived at the moment the session is minted: a pending authorization exists, its client is a non-expired test client, and the target is a non-admin test user granted that client's environment. Without a live pending authorization for a test client, no amount of posting a user id produces a session.

MFA, TOTP, email verification, password max-age and password complexity do not apply here. Every one of them is password- or email-bound and has no meaning for an account with no password.

To drive this from a script or CI job, see Local development with agents.

This is only as strong as the page is private

Anyone who can load a test client's sign-in page can sign in as any test user on it. That is the feature, not a gap — keep test users out of environments that hold production clients.

Passkeys (WebAuthn)

  • Register and sign in with device passkeys — Touch ID, Face ID, hardware keys.
  • Multi-device credential support.
  • Domain-based authenticator assertion with parent-domain fallback.
  • Check whether a user has a registered passkey via API.

Passkeys are self-service: users enroll, rename, remove, and verify-on-this-device from account settings. The client library can list/rename/remove passkeys, but creating/authenticating a passkey is a browser WebAuthn ceremony.

Multi-factor authentication (MFA)

Two MFA methods are supported, chosen per user at sign-in by availability:

Email OTP (site-wide)

When an admin enables MFA, every user with an email receives a server-generated 6-digit code by email. Attempt-capped via MFA_OTP_MAX_ATTEMPTS (default 5); challenge-response with short-lived tokens.

Authenticator app / TOTP (per-user)

A user enrolls an RFC 6238 authenticator (e.g. Google Authenticator) from account settings. The base32 secret is stored encrypted at rest (AES-GCM, key derived from AUTH_SECRET). Enrolling turns on MFA for that user even when the site-wide email toggle is off.

At sign-in, the available methods are computed for the user: with one method it is used directly; with both (site email MFA on and an authenticator enrolled) the user picks at a chooser and can fall back to an email code. The email code is only sent once email is actually chosen.

See the MFA & TOTP guide for enabling and operating MFA.

Email verification

  • New-user email verification flow with resend support.
  • The verification challenge is stored in D1 with an expiry.

On this page