Appearance
Identity pages reference
The Studio Explorer folder Security > Identity lists the seven system pages of the sign-in experience: the login page, the password recovery pair, the change-password page, the second-factor page and the two pages shown when access ends. They are ordinary pages, designed in the Page Designer and built from system blocks (core.system.*) that call the authentication API. This page explains how the pages are designed and flagged, what each flow does on the server, and the limits, lockout rules and messages that apply. The people and policy screens are in Users and access.
Where to find it. Security > Identity > the page name. Each of the seven Explorer entries opens the Pages list (page key pages); the pages themselves are edited like any other page (Build a page). The platform-launcher plugin ships the Login, Forgot Password and Reset Password pages. For Change Password, MFA Verification, Session Expired and Access Denied the blocks and the page flags exist, but no shipped page definition for them was found in the repository; create the page, or use the block on a page of your own.
How the identity pages are designed and flagged
| Concept | Definition |
|---|---|
| Public page | A page whose Access setting (Page Designer configuration panel) is Public. It renders with no session. Authenticated (the default) requires a session the backend still accepts; otherwise the runtime host redirects. Login, Forgot Password and Reset Password are public. |
| Login page flag | The configuration checkbox "Use as login page (the runtime host's auth gate redirects here)" stores isLoginPage: true in the page definition. |
| Session-expired flag | isSessionExpiredPage: true in the page definition. There is no checkbox for it; set it in the page's JSON. |
| Access-denied flag | isAccessDeniedPage: true in the page definition. Set it in the page's JSON. |
| Discovery | The runtime host finds the flagged page by scanning the workspace for the flag, across all applications and modules, using the latest published version of each page name. A page is found by flag, never by name or path. If nothing carries the flag the host falls back as described per page. |
| Session check switch | The workspace setting sessionCheckEnabled turns the authentication gate on or off for all pages; an application can override it (inherit, on or off). While off no redirect to these pages happens. |
System blocks
| Block type | Used by | Properties (default) | Events |
|---|---|---|---|
core.system.login-form | Login Page, MFA step inside the form | logoUrl, welcomeText ("Welcome back"), primaryColor (#2563EB), showRememberMe (true), showRegisterLink (false), showLanguageSelector (false), layout (stacked or split; default stacked) | onLoginSuccess, onLoginFailure, onMfaRequired |
core.system.social-login | Login Page (single sign-on buttons) | layout (buttons, dropdown, icons), providerFilter (list of provider ids to show), iconSize (40), glyphSize (18) | |
core.system.forgot-password | Forgot Password | instructionsText ("Enter your user ID and we'll send you a reset link."), buttonText ("Send reset link") | onRequestSent |
core.system.reset-password | Reset Password | policyText ("Password must be at least 8 characters, with a number and a symbol."), resetToken | onResetSuccess |
core.system.change-password | Change Password | policyText (same default) | onChangeSuccess |
core.system.mfa-verify | MFA Verification | instructionsText ("Enter the 6-digit code from your authenticator app."), challengeToken | onVerifySuccess, onVerifyFailure |
Each block performs a fixed call; it never navigates by itself. The page author wires its events to actions (for example navigate to homeRoute after onLoginSuccess). policyText and instructionsText are display text only; the real rules are the effective Password Policy. The login page of the platform-launcher plugin also remembers the user id on the device when the "remember me" box is ticked (devicePersistence, storage key rememberedUserId).
Shipped pages
| Page name | Route | Flags and access | Blocks |
|---|---|---|---|
platform-launcher-login | /login | access: public, isLoginPage: true | login form and single sign-on buttons |
platform-launcher-forgot-password | /forgot-password | access: public | core.system.forgot-password |
platform-launcher-reset-password | /reset-password/:resetToken | access: public; route parameter resetToken (string, required) | core.system.reset-password |
Each installing workspace gets its own copy of these pages, so a workspace can extend or replace them with its own branding without affecting others. All three belong to the module platform-launcher-login-module.
Login Page
The login page signs a person in with a user id and password, and hands the result to the next step: a second factor, a single sign-on redirect, or the person's landing page.
Where to find it. Security > Identity > Login Page (Pages list). Flag isLoginPage, route /login in the shipped page.
Fields and options. The form block collects:
| Field | Type | Required | Description |
|---|---|---|---|
| User id | Text | Yes | A username, email address or mobile number. |
| Password | Password | Yes | |
| CAPTCHA response | Text | Only after the failure threshold | See Limits. |
| Remember me | Checkbox | No | Shown when showRememberMe is on. |
The call. POST /api/v1/auth/login with headers X-Tenant-Id and optional X-Device-Fingerprint, body {userId, password, captchaResponse, sourceApp}. The server then applies, in this order:
- Both
userIdandpasswordmust be present: otherwise 400 "userId and password are required". - If the identifier does not exist in the requested workspace, the platform looks for it in the other workspaces and signs the person into the one that has it. The response
tenantIdstates which workspace was used; clients must use that value, not the one they sent. - Workspace security policy. Client address in the deny list, or not in a non-empty allow list: 403 "Sign-in is not allowed from this network". Outside the allowed hours (server time; a window may wrap midnight): 403 "Sign-in is not allowed at this time". Failed attempts at or above the CAPTCHA threshold with a blank
captchaResponse: 400 "CAPTCHA verification is required". Each rejection is recorded in Login History. - Credential check through the workspace's authentication provider (tenant-local by default). Failure: 401 "Invalid user id or password". The message is identical for an unknown user, a wrong password, a locked account, an expired password and an expired temporary password, so the response cannot be used to find out which accounts exist.
- Device recording. A device fingerprint (the
X-Device-Fingerprintheader, otherwise derived from browser and address) is recorded; the first sign-in from an unknown device sends a new-device alert email. - If the person has a verified MFA credential and the device is not trusted, no session is issued; the response carries
mfaRequired: trueand achallengeToken(see MFA Verification). - Otherwise a session is issued and a sign-in alert email is sent (unless the person opted out).
Success response (abridged):
json
{ "userId": "priya.nair", "sessionToken": "r4Nd0m...", "accessToken": "eyJhbGciOi...", "expiresAt": "2026-10-06T08:30:00Z",
"homeRoute": "/app/hcm/home/dashboard", "mfaRequired": false, "challengeToken": null, "orgUnits": ["12"], "tenantId": 2,
"user": { "username": "priya.nair", "email": "priya.nair@example.com", "mobile": null } }sessionToken is an opaque random token to send as X-Session-Token; accessToken is a signed token valid 15 minutes; expiresAt is the absolute session end. When mfaRequired is true, sessionToken, accessToken, expiresAt, homeRoute, orgUnits, tenantId and user are null.
Landing route (homeRoute). The first match wins: the application the login page belongs to (sourceApp and that application's own homeRoute); a route stored for the user; a route stored for the user's roles (first role alphabetically that has an entry); the workspace default (first module, then application, marked as login home). With sourceApp the login at /app/admin/user/login lands in that application directly. sourceApp is not carried across the MFA step, so a login that needs MFA falls back to the workspace default tiers.
Other sign-in paths.
| Path | Endpoint | Behaviour |
|---|---|---|
| Email code | POST /api/v1/auth/otp/request body {userId}, then POST /api/v1/auth/otp/verify body {userId, code} | The request always answers 202, whether or not the identifier exists. A 6-digit code valid 5 minutes (stored only as a hash) is emailed. Verification bypasses the password and the MFA step but applies the same network and hours checks; failure 401 "Invalid or expired code". |
| Single sign-on | GET /api/v1/auth/sso/providers (enabled providers, no secrets), POST /api/v1/auth/sso/discover (provider for an email), GET /api/v1/auth/sso/{providerId}/start, GET /api/v1/auth/sso/{providerId}/callback | Provider setup is documented in LDAP / SSO. The core.system.social-login block lists providers. |
| Organisation scope | PUT /api/v1/auth/session/org-scope body {orgUnitId} | Sets the active org unit of a session; 403 "Not a member of this org unit", 401 "Invalid or expired session". |
| Session check | GET /api/v1/auth/session header X-Session-Token | Returns {valid, userId, roles}; refreshes the idle clock. |
| Sign out | DELETE /api/v1/auth/logout header X-Session-Token | Revokes the session. |
Procedure. Make a custom login page the workspace login.
- Open Pages, create or copy a page, set Access to Public.
- Add a
core.system.login-formblock withwelcomeText"Welcome to Acme" andlayoutsplit; addcore.system.social-loginwithlayouticons. - In the page configuration tick "Use as login page (the runtime host's auth gate redirects here)".
- Wire
onLoginSuccessto a navigate action that uses the event'shomeRoute, and publish. - Open a protected page in a private window: the host redirects to the new page.
Statuses and lifecycle. A session is created at sign-in and ends by sign-out, revocation (Sessions screen, administrator, or concurrent-session limit), idle timeout (default 30 minutes), absolute timeout (default 12 hours) or a password change that policy requires. See Sessions.
Permissions. The endpoints are anonymous and need only X-Tenant-Id. A page flagged isLoginPage must be public.
Limits and behaviour.
| Control | Value |
|---|---|
| Lockout | After the policy's Max Failed Attempts (default 5) consecutive failures the account becomes LOCKED and an account-locked email is sent. It unlocks by itself after the lockout duration (default 15 minutes), doubling per repeat lockout when progressive lockout is on (capped at 24 hours), never when permanent lockout is on. A successful sign-in resets the counter. While locked, even the correct password is refused. |
| CAPTCHA | Required once failed attempts reach the workspace threshold (0 disables it). The gate is structural: any non-blank captchaResponse passes; no CAPTCHA vendor is connected. |
| Network and hours | Allow list and deny list are exact address matches (no ranges); a deny entry wins over an allow entry. Hours are whole hours of server time. |
| Rate limit | There is no per-address limit on /auth/login; account lockout and the CAPTCHA gate are the controls. The tenant-wide API ceiling also applies to /api/v1/**. |
| Concurrent sessions | When a sign-in would exceed the limit (default 5) the oldest sessions are revoked. |
| Timing | The workspace gate settings are GET and PUT /api/v1/security-policy (ipAllowList, ipDenyList, captchaAfterFailedAttempts, allowedLoginStartHour, allowedLoginEndHour, allowedCountries (stored, not enforced), enforceStrictSessions) and GET and PUT /api/v1/session-policy. |
Errors.
| Message | Cause | Fix |
|---|---|---|
| Invalid user id or password (401) | Wrong credentials, unknown user, locked or expired account | Check the id and password; ask an administrator to unlock. Login History shows the recorded reason. |
| Sign-in is not allowed from this network (403) | Address denied or not allowed | Change the security policy lists. |
| Sign-in is not allowed at this time (403) | Outside the allowed hours | Change the hours. |
| CAPTCHA verification is required (400) | Failure threshold reached | Supply captchaResponse. |
| userId and password are required (400) | Blank field | Fill both. |
Related. Sessions, Audit Logs (Login History), Password Policy.
Forgot Password
The Forgot Password page asks for a user id and emails a one-time reset link to the address on the account.
Where to find it. Security > Identity > Forgot Password. Shipped page platform-launcher-forgot-password, route /forgot-password, public. The login page's "Forgot Password?" link points here.
Fields and options. The block core.system.forgot-password has one input, the user id (username, email or mobile), the button text buttonText and the text instructionsText. After submitting it always shows its sent state.
The call. POST /api/v1/auth/forgot-password body {userId}.
| Behaviour | Detail |
|---|---|
| Response | Always 202 with no body, whether or not the account exists, so the page cannot be used to discover accounts. A blank userId is 400 "userId is required". |
| Token | 32 random bytes, URL-safe. Only its SHA-256 hash is stored. Valid 30 minutes, single use. |
| Rate limit | One live request per account at a time: a second request within 60 seconds of the previous one is silently ignored (still 202). |
| Delivery | An email of type password-reset-requested carrying the link <frontend base URL>/reset-password/<token>. The base URL is a server setting (set it to the public address of the workspace). |
| Log | The server writes the token to its INFO log as well, so a link is recoverable where no mail relay is configured. Treat the server log as sensitive. |
| Audit | The event PASSWORD_RESET_REQUESTED is recorded for an existing account. |
Procedure. An employee forgot the password: on the login page select "Forgot Password?", enter priya.nair, submit. After at most a few minutes the email arrives; if not, check that the account has an email address and that the notification channel is configured.
Errors. "userId is required" (400). A missing email is not reported to the caller: no email is sent and the response is still 202.
Related. Reset Password, Communications settings.
Reset Password
The Reset Password page sets a new password from the link in the email.
Where to find it. Security > Identity > Reset Password. Shipped page platform-launcher-reset-password, route /reset-password/:resetToken, public. The token is the route parameter resetToken and must be bound to the block property resetToken.
Fields. The block core.system.reset-password collects the new password and shows policyText.
The call. POST /api/v1/auth/reset-password/confirm body {resetToken, newPassword}, response 204.
- A blank token: 400 "resetToken is required".
- A token that is unknown, already used or older than 30 minutes: 400 "Invalid or expired reset token", and the event
PASSWORD_RESET_FAILEDis recorded. - The new password is checked against the person's effective policy and history. A violation returns 400 with the policy message and the token stays valid, so the person can retry.
- The password is replaced, an account in
LOCKEDorPASSWORD_EXPIREDreturns toACTIVE, the token is consumed, the eventPASSWORD_RESET_COMPLETEDis recorded and a password-changed email is sent.
The page author wires onResetSuccess to navigate to the login page.
Errors. "Invalid or expired reset token"; "This password was used recently - choose a different one"; the length, character and restriction messages listed under Password Policy.
Related. Forgot Password, Change Password.
Change Password
The Change Password page lets a signed-in person replace their password by proving the current one. It is for use inside an application (for example from a user menu) and also is the recovery path for an account in PASSWORD_EXPIRED.
Where to find it. Security > Identity > Change Password. Block core.system.change-password; the page must be authenticated, because the block takes the user id from the signed-in context.
Fields. Current password, New password, and the text policyText.
The call. PUT /api/v1/users/{id}/password body {currentPassword, newPassword}. No capability is checked; proof of the current password is the gate.
| Behaviour | Detail |
|---|---|
| Wrong current password | 401 "Current password is incorrect". The check uses the same failure counter as sign-in, so repeated guesses lock the account. |
| Expired password | An account in PASSWORD_EXPIRED can still prove its old password here; that is its recovery path. A temporary password that is more than 24 hours old cannot be used at all; an administrator must issue a new one. |
| New password | Checked against the effective policy and the history depth. 400 with the policy message or "This password was used recently - choose a different one". |
| Result | The password changes; an account in LOCKED or PASSWORD_EXPIRED returns to ACTIVE; the events PASSWORD_CHANGED is recorded and an email is sent. |
Accounts given a temporary password by an administrator cannot sign in normally: the next sign-in is refused until the password is changed here or by a reset link, because a forced change is pending. The block's event onChangeSuccess is for the author to wire.
Related. Users (Issue Temporary Password), Password Policy.
MFA Verification
The MFA Verification step asks for the 6-digit code of an authenticator application after the password has been accepted.
Where to find it. Security > Identity > MFA Verification. The login form block shows this step by itself (phase "mfa") when the server answers mfaRequired: true, so no separate page is needed. A page of its own uses the block core.system.mfa-verify, with challengeToken bound from the login form's onMfaRequired event.
Enrolment. A person enrols from the Users screen (Multi-Factor Authentication section, Users): TOTP (RFC 6238), 6 digits, 30-second period, issuer "ERP".
The calls.
POST /api/v1/auth/loginanswersmfaRequired: trueand achallengeTokenwhen the account has a verified credential and the device is not trusted. The challenge is valid 5 minutes; only its hash is stored. The sign-in attempt so far is recorded as a success.POST /api/v1/auth/mfa/verifybody{challengeToken, code}withX-Tenant-Id. A valid code consumes the challenge, issues the session (same response shape as login) and records a success. A wrong code records the failure reasonmfa-invalid-code.
Limits and behaviour. A trusted, non-revoked device skips the step. The platform keeps no counter of wrong codes; the five-minute challenge lifetime is the only limit, and the account lockout counter is not touched by MFA failures. The home route falls back to the workspace default (see Login Page).
Errors. "Invalid or expired challenge" (401: unknown, used or older than 5 minutes; not recorded in Login History); "Invalid MFA code" (401).
Related. Users (devices, trust), Audit Logs.
Session Expired
The Session Expired page is shown when a person had a session and the backend no longer accepts it, for example after the idle timeout, an administrator sign-out or the absolute limit.
Where to find it. Security > Identity > Session Expired. The page carries isSessionExpiredPage: true; it should be public.
How the host chooses. On opening any page whose Access is Authenticated, the runtime host checks the session token against GET /api/v1/auth/session.
| Situation | Redirect |
|---|---|
| No token was ever stored on the device | The page flagged isLoginPage |
| A token was stored but the backend rejects it | The page flagged isSessionExpiredPage, else the page flagged isLoginPage, else the inline "unauthorized" message of the host |
The page is typically a message with a button back to the login page.
Procedure. Set the session policy idle timeout to 1 minute (PUT /api/v1/session-policy body {"idleTimeoutMinutes":1,"absoluteTimeoutHours":12,"concurrentSessionLimit":5}), sign in, wait two minutes, then open a protected page: the host shows the Session Expired page.
Limits. The check is made when a page opens; calls from a page that is already open fail with HTTP 401 and are handled by that page. The session check can be switched off per workspace or per application (see above).
Access Denied
The Access Denied page is shown to a signed-in person who opens a page their licence does not cover.
Where to find it. Security > Identity > Access Denied. The page carries isAccessDeniedPage: true.
How the host chooses. After the authentication gate, a page that lists requiredFeatures is checked against the workspace licence: every listed feature must be present and true. A workspace that has never activated a licence is not blocked. If a feature is missing the host navigates to the page flagged isAccessDeniedPage; if none exists it shows the inline "not on your current plan" message and names the missing features.
Role-based denial is different. A page that a person's role denies through Screen Permissions is not sent here: the server answers screen-access-denied and the host treats the route as not found, so the existence of the page is not revealed. A denied API call returns api-access-denied (API Permissions).
Related. Screen Permissions, Permissions - how an access decision is made.
