Security
Overview
The security of the application is managed by permissions. Permissions are used to define the actions that the user group can perform in the application. Permissions are used to control the visibility of components, the behavior of components, and the behavior of the application. Permissions are defined in the app modules. Each component, action, and route can have a permission. If the user does not have the required permission, the component, action, or route will not be rendered or executed.
Permissions are assigned to roles. Roles are assigned to user groups. To receive a permission, the user must be a member of a user group that has a role with the permission.
On mobile cold boot, the renderer restores the last persisted permissions for the same user while the current-user request is still loading. This prevents permission-gated SDUI components from briefly disappearing after token or domain restoration. The fresh organization-scoped profile replaces the restored permissions as soon as it arrives; permissions are never reused for a different user.
Mobile authentication tokens are stored in the native secure store with iOS AFTER_FIRST_UNLOCK accessibility. This allows token rotation and cold-start restoration after the device has been unlocked once, including when the screen is currently locked. If a transient locked-keychain read returns no refresh token, the renderer falls back to the in-memory token. Network or temporary refresh failures preserve the stored session for a later retry; only an identity-server rejection that marks the refresh token expired clears the session.
Mobile token requests identify the installed app and device with User-Agent: CXTMS-Mobile/<version> (<platform>; <model>). The identity server uses this value to label the device in the user's signed-in-device list. Android model codes are prefixed with the manufacturer when needed; missing version or model information falls back safely without blocking authentication.
The identity server allows a configurable refresh-token reuse window (OpenIddict:RefreshTokenReuseLeewaySeconds, 600 seconds by default). This covers mobile clients that are suspended after the server rotates a token but before the response reaches the device; a retry with the previous token inside the window does not revoke the authorization chain. Expired and revoked OpenIddict tokens and tokenless authorizations older than 14 days are pruned by the openiddict-token-pruning Hangfire job each day at 03:00 UTC.
The mobile sign-in screen supports password, password reset, configured demo accounts, and available Google, Microsoft, and Apple providers. Provider slots remain stable while native readiness is resolved, and Apple is omitted when it is unavailable. Remember-me storage contains only the username preference; demo credentials bypass it. Authentication state and behavior are shared by both mobile login skins.
On the web, Google and Microsoft sign-in client IDs are resolved for each request from the active white-label configuration. A tenant-specific auth block takes precedence over runtime environment variables, which in turn take precedence over build-time values. This allows one deployed frontend to serve domains with different OAuth applications. Providers with no resolved client ID are not mounted or offered at sign-in; Microsoft may also set a tenant ID, otherwise its authority uses common.
auth: {
googleClientId: "tenant-google-client-id",
microsoftClientId: "tenant-microsoft-client-id",
microsoftTenantId: "tenant-directory-id"
}
When an account requires time-based one-time-password (TOTP) verification, both web and mobile treat the identity server's mfa_required response as a second login step. The client repeats the password grant with the original username and password plus totp_code; the password remains only in memory while this step is active. Returning to the password step clears the code. Invalid codes remain on the verification step, while account lockouts are shown explicitly.
Authenticated users can manage TOTP from profile settings on mobile: inspect current status, generate a setup secret and authenticator URI, enable TOTP with the account password and current code, disable it with a code, and regenerate recovery codes. Security status is always fetched from the network and is not persisted in the application query cache. Mutations are not automatically retried, because replaying a code can consume another verification attempt. Recovery codes are displayed only from the enable or regeneration response and should be stored immediately.
(Add a diagram to show the relationship between permissions, roles, and user groups)
Permissions
Permissions are defined in the app modules. Each component, action, and route can have a permission. If the user does not have the required permission, the component, action, or route will not be rendered or executed.
Example:
permissions:
- name: "System/Countries/Read"
description: "Read countries"
roles: # Assign roles to the permission
- "Administrator"
- "Operator"
Organization Contacts
Organization contacts are used to link application users to employees or other entities in the organization. Organization contacts are used to define the relationship between the user and the organization. For example, the user can be an employee, a customer, or a supplier.
Passkeys (WebAuthn)
Users can register passkey credentials and use the OpenID Connect passkey grant to authenticate without a password. Registration and assertion challenges are short-lived and scoped to the user and relying party. Credentials store the public key and signature counter; the private key remains on the user's authenticator.
The API supports listing, registering, renaming, and deleting the current user's passkeys. Relying-party identifiers are resolved from the request host or configured passkey settings. Treat registration and deletion as account-security operations requiring an authenticated user context.
The web renderer exposes passkey sign-in only when the tenant's enablePasskeys feature flag is enabled and the browser supports WebAuthn. It supports explicit sign-in and conditional browser autofill. Cancelling the browser prompt leaves the login form unchanged. The passkeys SDUI component uses the same feature and capability checks and renders its configured children together with the credential manager; unsupported clients render nothing.
GraphQL tenant gate
GraphQL requests are evaluated against the current organization. Normal users can access only organizations where they have membership, and deleted organizations are excluded. Members of the seeded System Administration organization with the Administrator role can perform authorized cross-organization administration. The system organization type cannot be claimed through a dynamic mutation.
When a request changes organization context, the API rebuilds the current-user data and its contact lookups for the new organization before continuing. This prevents stale per-request contact data from producing an empty user context or being inherited by background work queued from the request.