Skip to content

A JSON Schema on the users collection cannot require fields that /auth/register does not produce #662

Description

@ujibang

Brief overview

POST /auth/register builds the user document from five named body fields (firstName, lastName, teamName, email, password) and drops everything else before inserting. Since #657 the resulting document is validated against the collection's JSON Schema, but the schema can only constrain what the service already produces: an additional property sent in the body never reaches the document, so it can never satisfy the schema.

The consequence is that a schema declaring required: ["consents"] on the users collection makes registration permanently impossible — 400 ... violates schema for every request, with no body the client can send to satisfy it. This is currently asserted as expected behaviour in core/src/test/java/karate/accounts/register-schema-validation.feature:

# 'consents' is never produced by /auth/register, so the user document can
# never satisfy this schema

This is not only a gap: the published documentation already recommends the configuration that triggers it. docs/cloud/guards.adoc, "Example: Gating on Consents", step 1, tells the reader to put a schema with "required": ["_id", "password", "roles", "consents"] on the users collection and states that "this applies to sign-ups through /auth/register too". Today that configuration makes registration return 400 for every request.

This closes the scope note left open by #657 ("Unmapped body fields are dropped by the service before the document is built ... body-level rejection is not part of this issue"), and it is the exact use case that issue's rationale opened with: guaranteeing that a consents field is always present on user documents.

The drop itself predates the schema work — RegisterService has always assembled the document field by field. #657 added validation on top of it and documented the drop; it did not introduce it.

Rationale

An application's user document is not the one restheart-accounts knows about. Its shape is ultimately declared by the JSON Schema on the users collection, and the registration endpoint is the path that actually creates users. Today an application can declare that shape but cannot populate it, which leaves the schema useful only for constraining the fields the service itself writes.

Making the passthrough conditional on the schema being present is deliberate: the schema is what makes arbitrary additional properties safe to accept. With no schema there is nothing to constrain them, so they keep being dropped and behaviour is unchanged for every existing deployment.

Detailed documentation

1. RegisterService — the change

  • No jsonSchema metadata on the users collection: behaviour is exactly as today. Unmapped body fields are dropped, no validation runs.
  • jsonSchema metadata present: the remaining body properties are carried into the user document as-is, and the document is then validated against the schema as it already is today. The schema becomes the contract — additionalProperties: false rejects extensions, required: [...] mandates them.

Service-managed fields stay service-managed regardless of what the body carries, since registration is an anonymous endpoint and a blind merge would be privilege escalation:

field source
_id email
password body password, hashed
roles always ["$unauthenticated"] until email verification
emailVerificationToken, emailVerificationCreatedAt generated
profile.name, profile.surname firstName, lastName

A body attempting to set any of these must not be able to override them. Whether that is a 400 or a silent ignore is an implementation decision; the invariant is that the client cannot write them.

Note that a body property colliding with a service-managed path only matters for profile: a body carrying profile.avatarUrl should merge, while profile.name stays service-managed.

2. UpdateProfileService — no change

PATCH /auth/profile keeps updating profile.name / profile.surname only. It is a self-service, authenticated endpoint, and widening it would let a user rewrite their own additional properties — faking a consent acceptance, or changing a plan.

An application that needs to update other fields of the user document creates an ACL permission for PATCH /users/{email} instead, scoped with bson-request-whitelist(...) to the properties the user is allowed to write.

3. OAuth — no change

GET /auth/oauth/authorize/{provider} is a browser redirect with no client-controlled body, and OAuthCallback builds the user document from the provider profile. Neither changes.

For the consents example this works out without any OAuth-specific mechanism: a user registered through OAuth simply has no consents field, so a guard rule gating on consents blocks their requests. The user — authenticated, and therefore able to act — accepts the consents with a PATCH /users/{email} on their own document. The application's guard rules must let that request through, e.g.

method(PATCH) and path-template('/users/{email}') and equals(@user._id, ${email}) and bson-request-whitelist(consents)

which is the same permission an application would write for any other self-service field.

4. Tests to update

core/src/test/java/karate/accounts/register-schema-validation.feature, the scenario "registration fails with 400 when the schema requires a missing field". With a schema requiring consents, a body omitting them must still fail with 400, while a body carrying them must succeed with 201 and produce a document that contains them. A companion scenario should cover the unchanged no-schema case, where the same body still drops consents.

5. Documentation to update

In this repository:

  • RegisterService javadoc, the paragraph stating that unmapped body fields are dropped and never seen by the schema — after the fix that holds only when no schema is configured.

In restheart-website:

  • docs/accounts/user-registration.adoc — "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" and the surrounding notes on additionalProperties: false. Same conditional as the javadoc.
  • docs/mongodb-rest/json-schema-validation.adoc, the v9.7.0 note — it states that invariants such as "a consents field is always present" are enforced for documents created through restheart-accounts endpoints. Today such an invariant is enforced only in the sense that it makes registration impossible.
  • docs/cloud/guards.adoc, "Example: Gating on Consents" step 1 — the claim that the schema "applies to sign-ups through /auth/register too" becomes true with this fix. The example should also show the registration body carrying consents, which is what makes the schema satisfiable.
  • docs/accounts/consents-management.adoc — it currently documents a consentsSaver interceptor that intercepts the 201 from /auth/register and copies consents from the request body into the user document. With a schema configured that workaround becomes unnecessary; the page should say when it is still needed (no schema on the collection) and when it is not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugjavaPull requests that update Java code

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions