Edit Page

Security

RESTHeart Cloud

This 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-password always returns 202 Accepted regardless 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/register returns 409 Conflict for 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: $unset from 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).

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, S256 method) is used on every OAuth initiation. The code_verifier is never sent to the browser — it is server-side only.

  • state is validated with constant-time comparison on callback.

  • oauth_codes documents 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:

  • PUT and POST to /users are always rejected. Creating or fully replacing a user document must go through a dedicated restheart-accounts endpoint (/auth/register, /auth/verify, /auth/activate, /auth/accept-invite, the OAuth callback, …​), never the generic REST resource.

  • PATCH to /users is only permitted if every field it touches is under profile.*. 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

/auth/activate

PATCH

Invalid or expired invite token

/auth/reset-password

PATCH

Invalid or expired reset token

/auth/verify

GET

Redirect to error=invalid_token or error=token_expired

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