Security
RESTHeart CloudThis page documents the security design decisions in restheart-accounts.
Timing-attack mitigation
All token comparisons (invite tokens, verification tokens, password reset tokens, OAuth state) use constant-time string comparison. This prevents an attacker from inferring valid token prefixes by measuring response latency.
Standard string equality (String.equals) short-circuits on the first mismatch and leaks timing information. restheart-accounts uses MessageDigest.isEqual on the SHA-256 digests of both values — a standard constant-time technique.
Account enumeration prevention
-
POST /auth/forgot-passwordalways returns202 Acceptedregardless of whether the email is registered, verified, or unverified. The response body is the same in all cases — internally it sends either a password-reset email or a re-sent verification email depending on the account’s state, but the caller can’t tell which. -
POST /auth/registerreturns409 Conflictfor duplicate emails — this is a deliberate trade-off: registration UX requires telling the user the address is taken so they can recover their account.
Token design
All tokens (invite, verification, password reset, OAuth state) are:
-
Generated with
SecureRandom(256-bit entropy — equivalent to a UUIDv4 but longer). -
Single-use:
$unsetfrom the user document on first successful consumption. -
Scoped TTLs: invite and verification tokens expire after 7 days; password reset tokens after 1 hour; OAuth state after 600 seconds (MongoDB TTL index).
Password policy
Password strength is enforced server-side using the zxcvbn algorithm (Dropbox) at the point of:
-
Registration (
POST /auth/register) -
Invitation activation (
PATCH /auth/activate) -
Password reset (
PATCH /auth/reset-password)
The minimum score is configurable (minimumPasswordStrength, default 3 = "Strong").
Client-side zxcvbn checks are encouraged as UX guidance but are not trusted as security controls.
Session hygiene
Before issuing a JWT on invitation activation, the server clears any existing session. This prevents a logged-in user from accidentally activating an invitation as a different identity (session fixation variant).
Legal consent versioning
Consent tracking (terms & conditions, privacy policy versions) is not handled by the Accounts plugin. See Consents Management for guidance on implementing custom consent tracking. === Google OAuth security
-
PKCE (RFC 7636,
S256method) is used on every OAuth initiation. Thecode_verifieris never sent to the browser — it is server-side only. -
stateis validated with constant-time comparison on callback. -
oauth_codesdocuments auto-expire after 600 seconds via a MongoDB TTL index; they are also deleted immediately on use.
/users self-service write restriction
|
Caution
|
Since RESTHeart 9.6.0. |
When accountsInitializer is enabled, it unconditionally restricts what the generic MongoDB REST resource at /users can be used for, regardless of any ACL the application defines:
-
PUTandPOSTto/usersare always rejected. Creating or fully replacing a user document must go through a dedicatedrestheart-accountsendpoint (/auth/register,/auth/verify,/auth/activate,/auth/accept-invite, the OAuth callback, …), never the generic REST resource. -
PATCHto/usersis only permitted if every field it touches is underprofile.*. This is checked regardless of how the field is expressed — a full-document key, dot notation (profile.name), or a MongoDB update operator ($set,$push,$addToSet, …) — so{"$set": {"teams.0.role": "owner"}}is rejected exactly like{"teams": […]}.
Why: an ACL commonly grants an authenticated user permission to PATCH their own /users/{email} document, so they can self-edit their profile (name, avatar, …). Without this restriction, that same permission would let the user rewrite any field on their own document — including roles, team/teams, or a directly-set password — which is a privilege-escalation path (e.g. promoting themselves to owner in a team, or joining a team they were never invited to). This restriction closes that path unconditionally, for every application built on restheart-accounts, independent of how permissive its own ACL is.
This does not affect any restheart-accounts endpoint itself: every service that legitimately needs to set roles, team/teams, tokens, or a hashed password writes to MongoDB directly via the driver, not through the REST resource — so none of those flows are subject to this restriction.
If your application has its own reason to let certain accounts write more than profile.* to /users via the generic REST resource (for example, an internal admin console that manages roles/teams directly), configure users-unrestricted-roles in accountsConfig:
accountsConfig:
users-unrestricted-roles: [admin]
Accounts with any of the listed roles bypass this restriction entirely — both the PUT/POST block and the profile.* limit on PATCH. Leave it unset (the default) to apply the restriction to every caller, with no exceptions.
In a multi-tenant deployment, users-unrestricted-roles can also be overridden per team via override-accounts-users-unrestricted-roles — see Multi-tenancy for the full override mechanism.
accountsInitializer is enabled at the node level, independent of whether a given tenant has actually opted into restheart-accounts (e.g. RESTHeart Cloud enables it once per service node, but Sign-up Management is an opt-in feature per service). A tenant that never enabled it never opted into these opinions on /users either — so a deployment-layer interceptor running before authentication (e.g. TeamConfigInterceptor, at REQUEST_BEFORE_EXCHANGE_INIT) can attach override-accounts-signup-mgmt-enabled: false to skip the restriction entirely for that tenant, for every caller. This must be set before authentication: the veto is evaluated as part of authorization, which runs before any REQUEST_AFTER_AUTH interceptor gets a chance to run.
JWT claims
Which user-document fields are copied into an issued JWT, the claims that always survive a per-tenant override, and the denylist that no configuration can bypass, are described in Authentication — JWT claims.
They are documented there rather than here because they are not specific to restheart-accounts:
the same JwtIssuer applies them to every token this deployment issues — on /token, from the
accounts endpoints, and to OAuth authorization codes alike.
Brute-force protection
restheart-accounts relies on RESTHeart’s built-in bruteForceAttackGuard (sliding-window rate limiter per IP) for protection against token-guessing and credential stuffing.
Login endpoint
Failed login attempts on POST /token are counted automatically by the standard auth pipeline and trigger the guard.
Token-verification endpoints
Failed token checks on the following endpoints return 401 Unauthorized and are counted into the same AUTH metric registry via the built-in tokenFailedAuthInterceptor:
| Endpoint | Method | Counted when |
|---|---|---|
|
|
Invalid or expired invite token |
|
|
Invalid or expired reset token |
|
|
Redirect to |
Structural errors (missing fields, malformed JSON) still return 400 Bad Request and are not counted — only genuine token-guess failures feed the guard.
Configure bruteForceAttackGuard in restheart.yml:
/bruteForceAttackGuard/enabled: true
/bruteForceAttackGuard/max-failed-attempts: 5
/bruteForceAttackGuard/trust-x-forwarded-for: true # set false if no reverse proxy