Server-side forgot-password, recovery-email, and user-invite

Proposed features

1. Forgot-password (self-service password reset)

New unauthenticated endpoints:

POST /api/auth/forgot-password
Body: { "username": "alice" }
Response: 200 OK (always, to prevent user enumeration)

POST /api/auth/reset-password
Body: { "token": "<opaque>", "newPassword": "…" }
Response: 200 OK | 400 Bad Request (expired / already used)

Server behaviour:

  1. Look up the principal. If not found → silently no-op (anti-enumeration).

  2. Resolve recovery email (see feature 2 below). If none → silently no-op.

  3. Generate a 256-bit random token; store SHA-256(token) + TTL (e.g. 1 h) + account ID in the existing key-value store.

  4. Send a reset email via Stalwart’s own SMTP delivery.

  5. On reset-password: validate token with constant-time comparison, mark single-use, call the existing SysAccountPasswordUpdate path to set the new password.

Rate limiting: per-IP and per-username buckets — consistent with the existing is_http_anonymous_request_allowedframework.

Configuration (feature is off when not configured):

Key Purpose
auth.password-reset.enabled Master switch (default: false)
auth.password-reset.token-ttl Token lifetime (default: 1h)
auth.password-reset.from-email Sender address for reset emails
auth.password-reset.base-url Public origin used in the reset link

No external SMTP config needed — Stalwart already delivers mail.


2. Recovery-email on principal

A recoveryEmail field on the principal/account used solely for out-of-band identity actions (password reset, invites). It is notan email alias and does not receive regular mail.

Self-service endpoints (requires the user’s own session token):

GET  /api/account/recovery-email
Response: { "recoveryEmail": "[email protected]" | null }

PUT  /api/account/recovery-email
Body: { "recoveryEmail": "[email protected]" }
Response: 200 OK

Resolution order for password-reset emails:

  1. principal.recoveryEmail (new field)

  2. First address in principal.emails[]

Privilege model: reading and writing this field requires only the user’s own session — no admin token. This maps to new fine-grained permissions (e.g. SysAccountRecoveryEmailGet / SysAccountRecoveryEmailUpdate) that are granted to regular users by default, consistent with how SysAccountPasswordGet/Update works today.


3. User invite (admin-initiated onboarding)

An admin can trigger a welcome / account-activation email that generates the same kind of single-use reset token and delivers it to the user’s recovery email (or first email address).

New endpoint (requires a dedicated SysAccountInvite permission):

POST /api/admin/invite-user
Body: { "username": "bob" }
Response: 200 OK

The email contains a /reset-password?token=… link. The invited user sets their own password on first login — no password is ever set by the admin on their behalf.

stalwart-cli counterpart: stalwart-cli account invite <username>


What this is NOT

  • A full OAuth2 “device authorization” or PKCE flow — that already exists.

  • Mandatory: all three features are off by default, zero impact on existing deployments unless explicitly enabled.

  • Specific to Bulwark: these endpoints are generic HTTP; any JMAP client or CLI tool can use them.


Relation to the existing codebase

Existing piece How it is reused
RECOVERY_ADMIN_ID Unchanged — still the emergency CLI admin
SysAccountPasswordUpdate Called by the reset-password handler
validate_access_token / encode_access_token Pattern for short-lived reset tokens
is_http_anonymous_request_allowed Rate limiting for the unauthenticated endpoints
Stalwart SMTP delivery Sends reset / invite emails — no new SMTP config required
handle_account_request / AccountApiHandler Extended with recovery-email GET/PUT

Acceptance criteria

  • POST /api/auth/forgot-password — unauthenticated, rate-limited, always returns 200

    POST /api/auth/reset-password — validates token, single-use, updates credential

    recoveryEmail field on principal (self-service GET + PUT)

    POST /api/admin/invite-user — admin-only, sends activation email

    All three features are off by default

    No client ever holds a permanent admin token to perform these flows

    stalwart-cli can trigger an invite (stalwart-cli account invite <username>)

    Reset emails are delivered by Stalwart’s own mailer (no extra SMTP config)