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:
-
Look up the principal. If not found → silently no-op (anti-enumeration).
-
Resolve recovery email (see feature 2 below). If none → silently no-op.
-
Generate a 256-bit random token; store
SHA-256(token)+ TTL (e.g. 1 h) + account ID in the existing key-value store. -
Send a reset email via Stalwart’s own SMTP delivery.
-
On
reset-password: validate token with constant-time comparison, mark single-use, call the existingSysAccountPasswordUpdatepath 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:
-
principal.recoveryEmail(new field) -
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 200POST /api/auth/reset-password— validates token, single-use, updates credentialrecoveryEmailfield on principal (self-service GET + PUT)POST /api/admin/invite-user— admin-only, sends activation emailAll three features are off by default
No client ever holds a permanent admin token to perform these flows
stalwart-clican trigger an invite (stalwart-cli account invite <username>)Reset emails are delivered by Stalwart’s own mailer (no extra SMTP config)