- 1. Registration (new user)
- 2. Email Verification
- 3. Sign In
- 4. Password Reset
- 5. Invite New User (no existing account)
- 6. Invite Existing User (has account)
- 7. OAuth Login
- 8. Switch Team
- 9. List Teams
- 10. Create an Additional Team
- 11. List Team Members
- 12. Remove a Member
- 13. Update a Member’s Role
- 14. Update a Team
- 15. Delete a Team
- 16. Update Profile
- 17. Change Password
- MembershipProvider SPI Reference
Authentication Flows
RESTHeart CloudThis page documents all authentication and membership flows in restheart-accounts. Each flow lists the API endpoints, backend logic, SPI interactions, and the expected frontend behavior.
1. Registration (new user)
POST /auth/register โ public.
Request body:
{
"firstName": "Alice",
"lastName": "Rossi",
"teamName": "Acme",
"email": "alice@acme.com",
"password": "correct-horse-battery",
}
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: createInitialTeam(userId, teamName)
R->>M: create user (roles: $unauthenticated + verify token)
M-->>R: OK
R->>E: verification email
R-->>C: 201 Created
Flow:
-
Validate required fields and password strength (zxcvbn score >= 3).
-
Check email uniqueness โ
409 Conflictif already registered. -
MembershipProvider.createInitialTeam(userId, teamName)โ creates team. -
Create user with
roles: ["$unauthenticated"]and verification token. -
Send verification email.
-
Return
201 Created.
Consents: User consent tracking (e.g. terms & conditions, privacy policy) is not handled by the Accounts plugin. See Consents Management for guidance on implementing consent persistence via a response interceptor.
Frontend route: /auth/signup
2. Email Verification
GET /auth/verify?email=…&token=…&delivery=cookie|fragment โ public.
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
Note over C: clicks link in email
C->>R: GET /auth/verify?email=โฆ&token=โฆ
R->>M: find user, compare token (constant-time)
Note over R,M: TTL check (default 7 days)
R->>M: set roles: user, unset verify token
M-->>R: OK
alt delivery=cookie (default)
R-->>C: 302 redirect + JWT cookie โ frontend-app-url
else delivery=fragment
R-->>C: 302 redirect โ frontend-app-url#access_token=โฆ
end
Flow:
-
User clicks link in email.
-
Frontend route
/auth/verifyredirects browser to backend endpoint. -
Backend verifies token, sets
roles: ["user"], issues a JWT. -
302redirect tofrontend-app-url; the JWT is delivered according to thedeliveryquery parameter:-
delivery=cookie(or omitted, default) โ sets the JWT cookie. For same-origin setups. -
delivery=fragmentโ appends the JWT as a URL fragment (#access_token=…&token_type=Bearer&expires_in=…), same mechanism asGET /token/redirectand the OAuth callback. For cross-origin SPAs using Bearer token auth, where browsers block third-party cookies. No cookie is set.
-
-
Frontend detects authentication (cookie, or token read from
window.location.hash), shows success screen.
Frontend route: /auth/verify
3. Sign In
POST /token/cookie (Basic Auth) + GET /users/me
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: POST /token/cookie (Basic Auth)
R->>M: verify credentials
M-->>R: OK
R-->>C: JWT cookie
C->>R: GET /users/me
R->>M: load user profile
M-->>R: user document
R-->>C: user document
Flow:
-
Frontend sends Basic Auth credentials.
-
Backend sets JWT cookie.
-
Frontend calls
GET /users/meto load user profile.
Social login (Google/GitHub) is also available via OAuth buttons.
Frontend route: /auth/login
|
Note
|
POST /token/cookie is one of three ways RESTHeart can hand a token to a client. It’s the right choice when the frontend and RESTHeart share a registrable domain, as shown above โ for a cross-origin SPA, POST /token (token in the response body) is the usual alternative, since /token/cookie’s `HttpOnly cookie doesn’t travel reliably cross-origin (Safari blocks third-party cookies outright regardless of SameSite). See Choosing How to Deliver the Token for the full comparison, decision guide, and a sequence diagram for each of the three flows (cookie, token-in-body, and GET /token/redirect, 9.5+).
|
4. Password Reset
4a. Request reset link
POST /auth/forgot-password โ public. Always returns 202 (no email enumeration).
If the account has not completed email verification yet (still $unauthenticated), there is no password to reset: instead, a new emailVerificationToken is generated and the activation email is re-sent โ the account owner’s only self-service way to recover a lost or expired verification link (registration itself rejects the email as already taken). See Password Reset for details.
Request body:
{ "email": "alice@acme.com" }
Frontend route: /auth/forgot-pwd
4b. Apply new password
PATCH /auth/reset-password โ public.
Request body:
{
"email": "alice@acme.com",
"token": "<token from email>",
"password": "new-secure-password"
}
Auto-login on success. The delivery query parameter selects how the fresh JWT is handed back:
-
delivery=cookie(or omitted, default) โ sets therh_authHttpOnly JWT cookie. For same-origin setups. -
delivery=body(since 9.6.0) โ returns the token in the JSON response body (access_token,token_type,expires_in) and in theAuth-Tokenheader, and sets no cookie. For cross-origin SPAs using Bearer-token auth, so no secondPOST /token(re-sending the password) is needed.
Frontend route: /auth/reset-password?email=…&token=…
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
participant E as Email
Note over C,E: Step 1 โ Request reset link
C->>R: POST /auth/forgot-password {email}
R->>M: lookup user (result not revealed)
alt user found and verified
R->>M: generate + store reset token (TTL 1h)
R->>E: password reset email
else user found but not yet verified
R->>M: generate + store new emailVerificationToken
R->>E: verification email (re-sent)
end
R-->>C: 202 Accepted (always)
Note over C,M: Step 2 โ Apply new password
C->>R: PATCH /auth/reset-password {email, token, password}
Note over R: constant-time token compare + TTL check
Note over R: zxcvbn password strength check
R->>M: hash + save password, unset reset token
M-->>R: OK
R-->>C: 200 OK (JWT via cookie or body, per delivery)
5. Invite New User (no existing account)
POST /auth/invite โ requires owner role.
Request body:
{ "email": "bob@example.com", "role": "member" }
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
participant E as Email
O->>R: POST /auth/invite {email, role}
R->>M: check if user exists
M-->>R: not found
R->>M: check auth_invitations for pending invite (same email+org)
M-->>R: none found
R->>M: create user (roles: $unauthenticated, no password)
R->>M: create auth_invitations doc (isNewUser: true, TTL 7d)
R->>E: activation email โ /auth/activate?email=โฆ&token=โฆ
R-->>O: 201 Created
Flow:
-
Check if user exists.
-
Check
auth_invitationsfor a pending invite for the same email+org โ returns409if one exists. -
User does not exist: create user with
roles: ["$unauthenticated"](noinviteTokenon user document). -
Create invitation document in
auth_invitationswithisNewUser: true. -
Send invite email with link to
{frontend-url}/auth/activate?email=…&token=….
|
Note
|
Membership is not added at invite time. MembershipProvider.addMember() is called only when the user accepts the invitation (via password activation or OAuth), keeping the user unassociated until they explicitly accept.
|
5a. Read invitation metadata
GET /auth/invitation?email=…&token=… โ public.
The frontend calls this endpoint on page load to determine which UI to render (set-password form vs. log-in form) and to display the org name and role.
Both email and token parameters are required. The pair is known only to the invitee (delivered via private email link), so this endpoint cannot be used for enumeration.
Response 200 OK:
{
"email": "bob@example.com",
"teamName": "Acme Corp",
"role": "member",
"isNewUser": true,
"expiresAt": "2026-06-24T10:00:00Z"
}
Error responses: 400 (missing params) ยท 404 (invalid or expired token)
5b. Accept invitation โ set password
PATCH /auth/activate โ public.
Request body:
{
"email": "bob@example.com",
"token": "<token from email>",
"password": "correct-horse-battery"
}
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
Note over C: clicks link in email โ /auth/activate
C->>R: PATCH /auth/activate {email, token, password}
R->>M: find invitation by (email, token) in auth_invitations
Note over R: verify isNewUser=true, TTL check
Note over R: zxcvbn password strength check
R->>M: set password, roles โ user
R->>M: MembershipProvider.addMember(userId, teamId, role)
R->>M: delete invitation from auth_invitations
M-->>R: OK
R-->>C: 200 OK (JWT via cookie or body, per delivery)
Flow:
-
Find invitation by
(email, token)inauth_invitations(not on user document). -
Verify
isNewUser: trueโ returns400otherwise (wrong endpoint). -
Check expiry.
-
Set password, assign
roles: ["user"]. -
MembershipProvider.addMember(userId, teamId, role)โ adds user to the team and sets active team. -
Delete invitation from
auth_invitations(one-shot token). -
Issue JWT with the newly set active team.
-
Return
200 OK; the fresh JWT is delivered per thedeliveryquery parameter (auto-login). Consents are not managed by this endpoint. See Consents Management for details.
Auto-login delivery โ same delivery selector as /auth/reset-password:
-
delivery=cookie(or omitted, default) โ sets therh_authHttpOnly JWT cookie. For same-origin setups. -
delivery=body(since 9.6.0) โ returns the token in the JSON response body (access_token,token_type,expires_in) and in theAuth-Tokenheader, and sets no cookie. For cross-origin SPAs using Bearer-token auth.
Frontend route: /auth/activate?email=…&token=…
5c. Accept invitation โ via OAuth
The activation page (/auth/activate) shows OAuth buttons (Google/GitHub) with the pendingInviteToken passed as a query parameter to the OAuth authorize URL.
OAuth URL: {api-base-url}/auth/oauth/authorize/{provider}?pendingInviteToken=…
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant P as OAuth Provider
participant M as MongoDB
Note over C: on /auth/activate page โ clicks Sign in with Google
C->>R: GET /auth/oauth/authorize/google?pendingInviteToken=โฆ
R-->>C: 302 โ provider consent screen
C->>P: consent
P-->>C: 302 โ /auth/oauth/callback?code=โฆ&state=โฆ
C->>R: GET /auth/oauth/callback/google
Note over R: validate state, exchange code for profile
Note over R: user is $unauthenticated + pendingInviteToken present
R->>M: find invitation by (email, pendingInviteToken) in auth_invitations
R->>M: MembershipProvider.addMember(userId, teamId, role)
Note over R: activateViaOAuth() โ assigns roles:[user], stores consents
R->>M: delete auth_invitations doc
R-->>C: 302 + JWT cookie + #access_token= fragment โ frontend-app-url
Flow:
-
User clicks "Sign in with Google/GitHub".
-
OAuth callback detects
$unauthenticateduser withpendingInviteToken. -
Find invitation by
(email, pendingInviteToken)inauth_invitations, callMembershipProvider.addMember(userId, teamId, role)to set team. -
MembershipProvider.activateViaOAuth(userId, consents)โ assignsroles: ["user"], stores consents. -
Delete invitation from
auth_invitations. Issue JWT, set the auth cookie, and redirect tofrontend-app-urlwith the token also appended as a URL fragment (#access_token=…, since 9.5 โ see Choosing How to Deliver the Token).
6. Invite Existing User (has account)
POST /auth/invite โ same endpoint as above, different behavior for existing users.
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
participant E as Email
O->>R: POST /auth/invite {email, role}
R->>M: check if user exists
M-->>R: found
R->>M: isMember(userId, teamId)?
M-->>R: not a member
R->>M: create auth_invitations doc (isNewUser: false, TTL 7 days)
R->>E: invite email โ /invitations/accept?email=โฆ&token=โฆ
R-->>O: 201 Created
Flow:
-
User already exists โ do NOT add membership immediately.
-
Create an invitation document in
auth_invitationswithisNewUser: false. -
Send invite email with link to
{frontend-url}/invitations/accept?email=…&token=….
|
Note
|
All invitation tokens โ for both new and existing users โ are stored exclusively in the The
|
6a. Accept invitation โ log in then accept
POST /auth/accept-invite โ requires authentication.
Request body:
{ "token": "<token from email>" }
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
Note over C: clicks link โ /invitations/accept?email=โฆ&token=โฆ
Note over C: not logged in โ redirected to /auth/login
C->>R: POST /token/cookie (login)
R-->>C: JWT cookie
C->>R: POST /auth/accept-invite {token}
R->>M: find invitation by token in auth_invitations
Note over R: verify isNewUser=false, not expired, email matches caller
R->>M: MembershipProvider.addMember(userId, teamId, role)
R->>M: MembershipProvider.setActiveMembership(userId, teamId)
R->>M: delete invitation document
M-->>R: OK
R-->>C: 200 OK
Flow:
-
Find invitation by token in
auth_invitations(must not be expired). -
Verify
isNewUser: falseโ returns400otherwise (wrong endpoint). -
Verify the invitation email matches the authenticated user.
-
MembershipProvider.addMember(userId, teamId, role)โ adds user to team. -
MembershipProvider.setActiveMembership(userId, teamId)โ switches the user’s active team to the newly joined one. -
Delete the invitation document from
auth_invitations. -
Return
200 OK.
|
Note
|
The active team is switched to the accepted team so that the next login (or explicit team switch) places the user in the new context. |
Frontend route: /invitations/accept?email=…&token=…
If user is not logged in, AuthGuard redirects to /auth/login?returnUrl=/invitations/accept?email=…&token=….
6b. Accept invitation โ via OAuth
Existing users can also accept an invitation by logging in via OAuth with the pendingInviteToken.
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant P as OAuth Provider
participant M as MongoDB
Note over C: on /invitations/accept page โ clicks Sign in with Google
C->>R: GET /auth/oauth/authorize/google?pendingInviteToken=โฆ
R-->>C: 302 โ provider consent screen
C->>P: consent
P-->>C: 302 โ /auth/oauth/callback?code=โฆ&state=โฆ
C->>R: GET /auth/oauth/callback/google
Note over R: user is NOT $unauthenticated + pendingInviteToken present
R->>M: find invitation by (email, token) in auth_invitations
R->>M: MembershipProvider.addMember(userId, teamId, role)
R->>M: delete invitation document
R-->>C: 302 + JWT cookie + #access_token= fragment โ frontend-app-url
Flow:
-
User clicks "Sign in with Google/GitHub" on the
/invitations/acceptpage. -
OAuth callback detects the user is NOT
$unauthenticatedand apendingInviteTokenis present. -
Find invitation by
(email, pendingInviteToken)inauth_invitations. -
MembershipProvider.addMember(userId, teamId, role). -
Delete invitation. Issue JWT, set the auth cookie, and redirect to
frontend-app-urlwith the token also appended as a URL fragment (#access_token=…, since 9.5).
7. OAuth Login
GET /auth/oauth/authorize/{provider} โ GET /auth/oauth/callback/{provider}
sequenceDiagram
participant B as Browser
participant R as RESTHeart
participant P as OAuth Provider
participant M as MongoDB
B->>R: GET /auth/oauth/authorize/google
Note over R: generate CSRF state, store in oauth_codes (TTL 10 min)
R-->>B: 302 โ provider consent screen
B->>P: consent
P-->>B: 302 โ /auth/oauth/callback?code=โฆ&state=โฆ
B->>R: GET /auth/oauth/callback/google
Note over R: validate state (constant-time, findOneAndDelete)
R->>P: exchange code for access token
P-->>R: access token
R->>P: fetch user profile
P-->>R: profile
R->>M: upsert user document
Note over R: issue JWT
R-->>B: 302 + JWT cookie + #access_token= fragment โ frontend-url
Flow:
-
Frontend builds authorize URL with optional
pendingInviteToken,returnUrl,consentsAccepted. -
Backend stores CSRF state token, redirects to provider.
-
Provider redirects back with authorization code.
-
Backend exchanges code for user profile, finds or creates user.
-
If user is
$unauthenticated(new invited user): activate viaMembershipProvider.activateViaOAuth(). -
If user is NOT
$unauthenticatedandpendingInviteTokenis present (existing invited user): accept invitation viaauth_invitations, calladdMember(). -
Issue JWT, set the auth cookie, and redirect to
frontend-app-urlwith the token also appended as a URL fragment.
|
Note
|
Since RESTHeart 9.5, OAuthCallback hands the token back two ways on the same redirect: the rh_auth HttpOnly cookie (unchanged, for same-site frontends) and a #access_token=… URL fragment (for frontends using in-memory Bearer-token session handling, which can’t rely on a cross-origin cookie โ see Choosing How to Deliver the Token). The frontend reads location.hash on landing and clears it via history.replaceState.
|
8. Switch Team
POST /auth/switch-team โ requires authentication.
Request body:
{ "teamId": "<team-id>" }
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: POST /auth/switch-team {teamId}
R->>M: verify membership in teamId
R->>M: MembershipProvider.setActiveMembership(userId, teamId)
M-->>R: OK
Note over R: reissue JWT with team = {_id, role}
R-->>C: 200 OK (JWT via cookie or body, per delivery)
Calls MembershipProvider.setActiveMembership(userId, teamId) and reissues the JWT.
The reissued token is delivered per the delivery query parameter โ cookie (default, sets the rh_auth cookie) or body (since 9.6.0; returns access_token/token_type/expires_in in the response body plus the Auth-Token header, no cookie). The response body also carries the new active team and role.
9. List Teams
GET /auth/teams โ requires authentication.
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: GET /auth/teams
R->>M: MembershipProvider.listMemberships(userId)
M-->>R: memberships
R-->>C: team list
Returns all teams the user belongs to via MembershipProvider.listMemberships(userId).
[
{ "id": { "$oid": "64a1b2c3..." }, "name": "Acme Corp", "description": "Main workspace", "role": "owner", "active": true },
{ "id": { "$oid": "74b2c3d4..." }, "name": "Side Gig", "role": "member", "active": false }
]
The description field is included when the team document has a non-null description. Omitted otherwise.
10. Create an Additional Team
POST /auth/teams โ requires authentication. Same URI as 9. List Teams, distinguished by HTTP method (GET vs POST).
Lets an already-registered user spin up an additional workspace โ e.g. "New Workspace" the way Slack/Notion let you โ the same way createInitialTeam does during signup, except this can be called any number of times.
Request body:
{ "teamName": "New Workspace" }
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: POST /auth/teams {teamName}
R->>M: MembershipProvider.createTeam(userId, teamName)
Note over R,M: adds caller as owner, sets team as newly active
M-->>R: OK
Note over R: reissue JWT with team = {_id, role}
R-->>C: 201 Created (JWT via cookie or body, per delivery)
Flow:
-
Validate
teamNameโ400if missing or blank. -
MembershipProvider.createTeam(userId, teamName)โ creates the team, adds the caller as owner, and sets it as the newly active membership (unlikecreateInitialTeam, which only activates a team if the user has none yet). -
Reissue the JWT with the new team claim, delivered per the
deliveryquery parameter (samecookie/bodyselector as 8. Switch Team). -
Return
201 Createdwith{ "id": …, "name": …, "role": "owner" }.
MembershipProvider SPI: createTeam(userId, teamName)
11. List Team Members
GET /auth/team/members โ requires authentication.
Returns the caller’s active team’s member list (name, email, role, joinedAt), denormalized against each member’s profile. There is no caller-supplied team filter โ always the caller’s own active team, avoiding the need for separate authorization logic for "can I see team X’s roster."
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: GET /auth/team/members
R->>M: MembershipProvider.listTeamMembers(teamId)
Note over R,M: reads team.members[], batch-fetches users for display names
M-->>R: members
R-->>C: member list
Response 200 OK:
[
{ "email": "alice@acme.com", "name": "Alice Rossi", "role": "owner", "joinedAt": "2026-01-01T00:00:00Z" },
{ "email": "bob@acme.com", "name": "Bob Smith", "role": "member", "joinedAt": "2026-01-02T00:00:00Z" }
]
MembershipProvider SPI: listTeamMembers(teamId)
12. Remove a Member
DELETE /auth/remove-member โ requires owner role.
Request body:
{ "email": "bob@example.com" }
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
O->>R: DELETE /auth/remove-member {email}
R->>M: verify caller is owner of active team
R->>M: verify target is a member (404 otherwise)
Note over R: reject if owner tries to remove themselves (400)
R->>M: MembershipProvider.removeMember(userId, teamId)
M-->>R: OK
R-->>O: 200 OK
Flow:
-
Verify caller is
ownerof their active team. -
Check target is a member of that team โ
404otherwise. -
Prevent owner from removing themselves โ
400. -
MembershipProvider.removeMember(userId, teamId)โ removes membership on both user and team side; clears active team if it was the removed one. -
Return
200 OK.
MembershipProvider SPI: removeMember(userId, teamId)
13. Update a Member’s Role
PATCH /auth/member-role โ requires owner role.
Request body:
{ "email": "bob@example.com", "role": "member" }
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
O->>R: PATCH /auth/member-role {email, role}
R->>M: verify caller is owner of active team
Note over R: validate role (must be memberRoleName or ownershipRole)
R->>M: verify target is a member (404 otherwise)
R->>M: MembershipProvider.updateMemberRole(userId, teamId, newRole)
M-->>R: OK
R-->>O: 200 OK
role must be the configured member-role-name (default "member") or ownershipRole.
Ownership transfer is not supported via this endpoint.
Flow:
-
Verify caller is
ownerof their active team. -
Validate role value โ
400if notmemberRoleNameorownershipRole. -
Check target is a member of that team โ
404otherwise. -
MembershipProvider.updateMemberRole(userId, teamId, newRole)โ updates role on both user and team side. -
Return
200 OK.
MembershipProvider SPI: updateMemberRole(userId, teamId, newRole)
14. Update a Team
PATCH /auth/team โ requires owner role. Same URI as 15. Delete a Team, distinguished by HTTP method (PATCH vs DELETE).
Request body (partial updates allowed):
{ "name": "Acme Corp", "description": "Our team workspace" }
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
O->>R: PATCH /auth/team {name?, description?}
R->>M: verify caller is owner of active team
R->>M: MembershipProvider.updateTeam(teamId, name, description)
M-->>R: OK
R-->>O: 200 OK
Flow:
-
Verify caller is
ownerof their active team โ403otherwise. -
Validate body โ
400if neithernamenordescriptionis present, or ifnameis blank. -
MembershipProvider.updateTeam(teamId, name, description)โ partial$set; anullparameter leaves that field unchanged. -
Return
200 OK.
MembershipProvider SPI: updateTeam(teamId, name, description)
15. Delete a Team
DELETE /auth/team โ requires owner role.
Deletes the caller’s active team, but only if it has no other members. The "no other members" invariant is enforced atomically, server-side: a client-side pre-check (list members, see only the caller, then delete) would be a race condition against a concurrent invite acceptance.
sequenceDiagram
participant O as Owner
participant R as RESTHeart
participant M as MongoDB
O->>R: DELETE /auth/team
R->>M: verify caller is owner of active team
R->>M: MembershipProvider.deleteTeam(userId, teamId)
Note over R,M: atomic findOneAndDelete guarded by members.size <= 1
alt team was empty
M-->>R: deleted; caller's membership/active-team pointer cleared
R-->>O: 200 OK
else team still has other members
M-->>R: not deleted
R-->>O: 409 Conflict
end
Flow:
-
Verify caller is
ownerof their active team โ403otherwise. -
MembershipProvider.deleteTeam(userId, teamId)โ atomically deletes the team only if it has at most one member; returnsfalse(does not throw) otherwise. -
If deletion failed because the team still has other members, return
409 Conflict. If it failed because the team no longer exists (already deleted by a concurrent/duplicate request), return404 Not Found. -
On success, the caller’s own membership entry and active-team pointer are cleared โ they are left with no active team and must create or be invited into a new one.
-
Return
200 OK.
MembershipProvider SPI: deleteTeam(userId, teamId)
16. Update Profile
PATCH /auth/profile โ requires authentication.
Self-service update of the caller’s own profile.name / profile.surname fields, exposed to clients as firstName / lastName (matching /auth/register’s request body). This dedicated endpoint self-registers its own ACL allow rule at startup โ same pattern as every other `restheart-accounts endpoint โ instead of relying on generic PATCH /users/{email} plus a tenant-configured ACL allow rule, which isn’t guaranteed to exist (see the /users Access section on the overview page).
Request body (partial updates allowed):
{ "firstName": "Alice", "lastName": "Rossi" }
Flow:
-
Validate body โ
400if neitherfirstNamenorlastNameis present, or if either is blank. -
$set profile.name/profile.surnameon the caller’s own user document (writes directly via the MongoDB driver, not through the generic REST resource). -
Return
200 OK.
Frontend route: /account (settings page)
17. Change Password
PATCH /auth/change-password โ requires authentication.
Lets a logged-in user change their password in-session, given their current password โ unlike PATCH /auth/reset-password, which is the public, unauthenticated "forgot password" flow (emailed one-shot token). Before this endpoint, a logged-in user could only change their password by calling /auth/forgot-password on themselves and completing the email round-trip.
Request body:
{ "currentPassword": "correct-horse-battery", "newPassword": "new-secure-password" }
sequenceDiagram
participant C as Client
participant R as RESTHeart
participant M as MongoDB
C->>R: PATCH /auth/change-password {currentPassword, newPassword}
R->>M: find user
alt account has a password
Note over R: bcrypt-verify currentPassword โ 401 if it doesn't match
else account has no password yet (e.g. OAuth-only)
Note over R: currentPassword not checked
end
Note over R: minimum length check on newPassword
R->>M: hash + save newPassword
M-->>R: OK
R-->>C: 200 OK
Flow:
-
Look up the caller’s own user document โ always the authenticated principal, never an arbitrary user (this endpoint requires authentication and never takes a target user in the request).
-
If the account already has a password, verify
currentPasswordagainst the stored bcrypt hash โ401if it doesn’t match. -
If the account has no password yet, skip that check โ see "OAuth-only accounts" below.
-
Enforce minimum password length on
newPasswordโ400otherwise. -
Hash and save the new password.
-
Return
200 OK.
|
Important
|
OAuth-only accounts. A user who signed up via This is safe without any extra guard because the check above (step 1) already scopes the whole request to the caller’s own document โ there is no A frontend should not assume every account has a password: the user document carries a |
|
Note
|
Does not invalidate other active sessions/JWTs โ out of scope for JWT-based auth unless a token-versioning/blacklist mechanism is added separately. |
Frontend route: /account (settings page)
MembershipProvider SPI Reference
| Method | Called by | Cloud implementation |
|---|---|---|
|
|
Creates org in |
|
|
Checks |
|
|
Adds teamId to user’s |
|
JWT issuance, |
Reads |
|
|
Iterates |
|
|
Sets |
|
|
|
|
|
Positional |
|
OAuth callback for invited users |
Assigns |
|
|
Reads org’s |
|
|
Creates org in |
|
|
Partial |
|
|
Atomic conditional delete of the org document, guarded by member-count; clears user’s org membership on success |