User Registration & Email Verification
RESTHeart CloudFlow Overview
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
participant E as Email
C->>R: POST /auth/register
Note over R: validate fields, zxcvbn score ≥ 3
R->>M: check email uniqueness
R->>M: create user (roles: $unauthenticated + verify token)
M-->>R: OK
R->>E: verification email
R-->>C: 201 Created
Note over C,E: user clicks link in email
C->>R: GET /auth/verify?email=…&token=…
R->>M: constant-time token compare + TTL check
R->>M: set roles → user, unset verify token
M-->>R: OK
R-->>C: 302 + JWT cookie → frontend app URL
Registration flow
POST /auth/register is the public signup endpoint. It creates a new user and a new team in a single atomic operation.
Request
POST /auth/register
Content-Type: application/json
{
"firstName": "Alice",
"lastName": "Rossi",
"teamName": "Acme",
"email": "alice@acme.com",
"password": "correct-horse-battery"
}
Server-side steps
-
Validate all required fields.
-
Enforce password strength via zxcvbn (score ≥
minimumPasswordStrength, default 3). -
Check email uniqueness —
409 Conflictif already registered. -
Generate a cryptographically random
verificationToken(256-bit) andverificationCreatedAttimestamp. -
Create the user document with
roles: ["$unauthenticated"]and the verification token. -
Validate the user document against the collection’s JSON Schema (if configured — see below).
-
Create a
membershipdocument linking the user to the team asowner. -
Send a verification email containing a one-time link:
{baseAppUrl}/auth/verify?email=alice@acme.com&token=<verificationToken> -
Return
201 Created.
Response
{ "message": "Registration successful. Check your email to verify your address." }
Error responses
| Status | Reason |
|---|---|
|
Missing or invalid fields; password too weak; document violates collection JSON Schema |
|
Email already registered |
|
A JSON Schema is configured on the users collection but cannot be applied |
JSON Schema validation on registration
|
Note
|
Available from v9.7.0
Starting from RESTHeart v9.7.0, the registration endpoint validates the user document against the collection’s JSON Schema (if configured).
|
If the users collection has a jsonSchema metadata configured (via the _properties collection), the registration endpoint validates the user document against that schema before inserting it. This ensures that invariants declared on the users collection — for example that a consents field is always present — are enforced even for documents created through the registration endpoint, which bypasses the MongoService pipeline.
How it works
-
The user document is built from the request body (see the mapping table below).
-
The collection properties are read from
_properties._properties.{usersCollection}in the effective database. Both the database and the collection are sourced frommongoRealmAuthenticator(users-dbandusers-collection). -
If a
jsonSchemametadata is present, the document is validated against the referenced schema, resolved through thejson-schemasprovider. -
On validation failure, the endpoint returns
400with the list of violations, and no user is created. -
If no schema is configured, validation is skipped (opt-in).
Validation fails closed
Once the jsonSchema metadata is on the collection, it is honoured or the request fails. If the schema cannot be applied — it is missing from the schema store, the metadata is malformed, or restheart-mongodb is not deployed so the json-schemas provider is unavailable — the endpoint returns 500 and no user is created.
A typo in schemaId therefore surfaces as an error rather than silently disabling the invariant the schema was written to enforce.
What the schema sees
The document is validated as it is inserted, which is not its final shape. createInitialTeam runs right after the insert and adds teams and team to the user document. Consequently:
-
a schema with
required: ["team"]always fails, even though the stored document does end up with the field; -
a schema with
additionalProperties: falsepasses validation but does not describe the document a moment later.
Schemas meant for the registration path should declare teams and team as optional.
When no jsonSchema metadata is configured on the users collection, fields of the request body that are not in the mapping table are dropped before the document is built, so the schema never sees them and cannot reject them — with or without additionalProperties: false.
When a jsonSchema is configured, the remaining body properties are carried into the user document as-is before validation. The schema becomes the contract: additionalProperties: false rejects extensions, required: […] mandates them. Service-managed fields (_id, password, roles, profile.name, profile.surname, emailVerificationToken, emailVerificationCreatedAt) are never overwritten by the body.
Application-level fields such as consents should be declared optional in the schema — their absence is enforced by guard rules, not by the schema. The schema validates the format when the field is present. See Gating on Consents for the full pattern.
Violation messages are returned as-is, in the schema’s vocabulary. A body that omits firstName produces #/profile/name: required key [name] not found, not a message mentioning firstName.
Request body to user document mapping
| Request body field | User document field |
|---|---|
|
|
|
|
|
|
|
|
|
not stored on the user document — a team document is created by |
|
Note
|
The schema validates the stored document, not the request body. A schema that requires |
Example
# Create a schema that validates consents format (optional) and allows OAuth fields
http POST :8080/_schemas Authorization:"Basic ..." \
_id=user-with-consents \
'$schema'="http://json-schema.org/draft-07/schema#" \
type=object \
required:='["_id", "password", "roles", "profile"]' \
properties:='{
"profile": {
"type": "object",
"required": ["name", "surname"],
"properties": {
"name": { "type": "string" },
"surname": { "type": "string" },
"avatarUrl": { "type": "string" }
}
},
"consents": {
"type": "object",
"required": ["terms", "privacy"],
"properties": {
"terms": { "type": "boolean" },
"privacy": { "type": "boolean" }
}
},
"socialAuths": { "type": "array" },
"teams": { "type": "array" },
"team": { "type": "object" }
}'
# Apply schema to users collection
http PATCH :8080/users Authorization:"Basic ..." \
jsonSchema:='{"schemaId":"user-with-consents"}'
# Registration with consents in the body — passes validation
http POST :8080/auth/register Authorization:"Basic ..." \
firstName=Alice lastName=Smith teamName=Acme \
email=alice@example.com password=Password123! \
consents:='{"terms":true,"privacy":true}'
# → 201 Created (consents stored on the user document)
# Registration without consents — schema is fine, guard handles absence
http POST :8080/auth/register Authorization:"Basic ..." \
firstName=Bob lastName=Jones teamName=Acme \
email=bob@example.com password=Password123!
# → 201 Created (no consents field — guard rule blocks subsequent requests)
|
Note
|
Consents management: User consent tracking is not handled by the Accounts plugin. See Consents Management for details. |
Email verification
GET /auth/verify?email=…&token=… is sent as a link in the registration email.
Server-side steps
-
Extract
email,tokenand the optionaldeliveryparameter from the query string. -
Find the user by email.
-
Compare
tokenagainstverificationTokenusing constant-time comparison (timing-attack mitigation). -
Check that
verificationCreatedAtis within the TTL (verificationTokenTtlDays, default 7 days). -
Assign the system ACL role from
accountsConfig.default-role(default:user) — this replaces the$unauthenticatedrole assigned at registration.$unsetverificationToken and verificationCreatedAt. -
Issue a JWT and deliver it according to
delivery(see below). -
Redirect to
{frontendAppUrl}.
Token delivery: cookie vs URL fragment
The optional delivery query parameter selects how the JWT is handed to the frontend:
delivery |
Behavior |
|---|---|
|
Default. Sets the auth cookie and redirects to |
|
Redirects to |
GET /auth/verify?email=...&token=... → cookie (default)
GET /auth/verify?email=...&token=...&delivery=cookie → cookie
GET /auth/verify?email=...&token=...&delivery=fragment → URL fragment
With delivery=fragment no cookie is set. The fragment uses the same mechanism as GET /token/redirect and the OAuth callback: since a URL fragment is never sent to any server, the token does not leak into access logs or the Referer header. The frontend reads it from window.location.hash and stores it (e.g. in localStorage) for use in the Authorization: Bearer header.
Expired token
If the token has expired, the endpoint returns 400 with a message inviting the user to request a new verification email from the login page.
|
Note
|
Re-sending a verification email is triggered by POST /auth/resend-verify (not yet implemented in v1 — users can contact support or attempt login which will prompt re-verification).
|