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",
"consents": { "termsAndConditionsAndPrivacyPolicy": true, "unfairTermsAndConditions": true }
}
Flow:
-
Validate required fields and password strength (zxcvbn score >= 3).
-
Check email uniqueness —
409 Conflictif already registered. -
MembershipProvider.createInitialTeam(userId, teamName)— creates team/tenant. -
Create user with
roles: ["$unauthenticated"]and verification token. -
Send verification email.
-
Return
201 Created.
Consents: POST /auth/register does not persist user consents.
Use a response interceptor on the deployment layer to read the consents object from the request body and persist it on the user document.
See the AccountsConsentsSaver interceptor in restheart-cloud-server for a reference implementation.
Frontend route: /auth/signup
2. Email Verification
GET /auth/verify?email=…&token=… — public.
Flow:
-
User clicks link in email.
-
Frontend route
/auth/verifyredirects browser to backend endpoint. -
Backend verifies token, sets
roles: ["user"], issues JWT cookie. -
302redirect tofrontend-app-url. -
Frontend detects authentication, shows success screen.
Frontend route: /auth/verify
3. Sign In
POST /token/cookie (Basic Auth) + GET /users/me
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
4. Password Reset
4a. Request reset link
POST /auth/forgot-password — public. Always returns 202 (no email enumeration).
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 (JWT cookie set by the plugin).
Frontend route: /auth/reset-password?email=…&token=…
5. Invite New User (no existing account)
POST /auth/invite — requires owner or admin role.
Request body:
{ "email": "bob@example.com", "role": "member" }
Flow:
-
Check if user exists.
-
If user does not exist: create user with
roles: ["$unauthenticated"], generate invite token, add membership viaMembershipProvider.addMember(). -
Send invite email with link to
{frontend-url}/auth/activate?email=…&token=….
MembershipProvider SPI: addMember(userId, tenantId, role) — adds user to team.
5a. Accept invitation — set password
PATCH /auth/activate — public.
Request body:
{
"email": "bob@example.com",
"token": "<token from email>",
"password": "correct-horse-battery"
}
Flow:
-
Verify invite token.
-
Set password, assign
roles: ["user"]. -
Auto-login (JWT cookie).
Consents are not managed by this endpoint.
Use a response interceptor to persist consents from the request body (see AccountsConsentsSaver).
Frontend route: /auth/activate?email=…&token=…
5b. 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=…
Flow:
-
User clicks "Sign in with Google/GitHub".
-
OAuth callback detects
$unauthenticateduser. -
MembershipProvider.activateViaOAuth(userId, consents)— assignsroles: ["user"], stores consents. -
JWT cookie set, redirect to
frontend-app-url.
If the OAuth email does not match the invited email, the callback redirects to {frontend-error-url}?reason=….
6. Invite Existing User (has account)
POST /auth/invite — same endpoint as above, different behavior for existing users.
Flow:
-
User already exists — do NOT add membership immediately.
-
Create an invitation document in the
auth_invitationscollection with{ email, token, orgId, role, createdAt, expiresAt }. -
Send invite email with link to
{frontend-url}/invitations/accept?token=….
|
Note
|
The |
6a. Accept invitation — existing user
POST /auth/accept-invite — requires authentication.
Request body:
{ "token": "<token from email>" }
Flow:
-
Find invitation by token in
auth_invitationscollection (must not be expired). -
Verify the invitation email matches the authenticated user.
-
MembershipProvider.addMember(userId, orgId, role)— adds user to team. -
Delete the invitation document from
auth_invitations. -
Return
200 OK.
Frontend route: /invitations/accept?token=…
If user is not logged in, AuthGuard redirects to /auth/login?returnUrl=/invitations/accept?token=….
7. OAuth Login
GET /auth/oauth/authorize/{provider} → GET /auth/oauth/callback/{provider}
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(invited): activate viaMembershipProvider.activateViaOAuth(). -
Issue JWT cookie, redirect to
frontend-app-url.
If there was a pendingInviteToken but the user is not $unauthenticated (email mismatch), redirect to {frontend-error-url}?reason=….
8. Switch Tenant
POST /auth/switch-tenant — requires authentication.
Request body:
{ "tenantId": "<tenant-id>" }
Calls MembershipProvider.setActiveMembership(userId, tenantId) and reissues JWT.
9. List Tenants
GET /auth/tenants — requires authentication.
Returns all teams the user belongs to via MembershipProvider.listMemberships(userId).
10. Remove a Member
DELETE /auth/remove-member — requires owner or admin role.
Request body:
{ "email": "bob@example.com" }
Flow:
-
Verify caller is
owneroradminof their active tenant. -
Check target is a member of that tenant —
404otherwise. -
Prevent owner from removing themselves —
400. -
MembershipProvider.removeMember(userId, tenantId)— removes membership on both user and team side; clears active tenant if it was the removed one. -
Return
200 OK.
MembershipProvider SPI: removeMember(userId, tenantId)
11. Update a Member’s Role
PATCH /auth/member-role — requires owner or admin role.
Request body:
{ "email": "bob@example.com", "role": "admin" }
role must be the configured member-role-name (default "member") or "admin".
Ownership transfer is not supported via this endpoint.
Flow:
-
Verify caller is
owneroradminof their active tenant. -
Validate role value —
400if notmemberRoleNameor"admin". -
Check target is a member of that tenant —
404otherwise. -
MembershipProvider.updateMemberRole(userId, tenantId, newRole)— updates role on both user and team side. -
Return
200 OK.
MembershipProvider SPI: updateMemberRole(userId, tenantId, newRole)
MembershipProvider SPI Reference
| Method | Called by | Cloud implementation |
|---|---|---|
|
|
Creates org in |
|
|
Checks |
|
|
Adds orgId to user’s |
|
JWT issuance, |
Reads |
|
|
Iterates |
|
|
Sets |
|
|
|
|
|
Positional |
|
OAuth callback for invited users |
Assigns |