docs
/
AppEngine API

Two-factor and device security

2FA setup and verification, device fingerprinting, trust and blocking, and login history.

Every sign-in fingerprints the device and checks it against what the account has used before. 2FA sits on top of that.

Device fingerprinting

On each sign-in:

1. Fingerprint and identify generateDeviceFingerprint(meta) builds a fingerprint from request metadata; generateDeviceId(fingerprint, userId) derives a stable per-user device id.

2. Check the blocklist A blocked device is 403 — This device has been blocked. Please contact support. before credentials matter.

3. Compare with known devices isKnownDevice(orgId, userId, 'user', deviceId).

4. Register or update Unknown devices are registered with IP and geo location, and — unless alertOnNewDeviceLogin is false — trigger an alert email. Known devices just get their activity timestamp bumped.

5. Record the login recordLogin(orgId, userId, 'user', deviceId, ip, location, success, method) for the audit trail, alongside an activity record. Location comes from the request metadata — realIp, host, country, city, subdivisions, userAgentString.

Managing devices

GET/profile/security/devicesJWT
GET/profile/security/devices/currentJWT
POST/profile/security/devices/:deviceId/trustJWT
POST/profile/security/devices/:deviceId/blockJWT
DELETE/profile/security/devices/:deviceIdJWT

Trusting a device suppresses repeated 2FA challenges from it. Blocking refuses it outright.

Login history

GET/profile/security/login-historyJWT

Every attempt — successful or not — with device, IP, location and method.

Two-factor authentication

When it is required

2FA triggers if either the org sets securitySettings.enableTwoFactorForUsers or the user enabled it. isTwoFactorRequired(orgId, userId, 'user', deviceId) then decides per device, so a trusted device is not challenged every time.

Separately, securitySettings.enableNewDeviceAuthentication forces an email challenge on an unrecognized device even when 2FA is otherwise off.

The challenge

Sign-in returns instead of a token:

{ "requiresTwoFactor": true, "challengeToken": "…", "twoFactorMethod": "email", "message": "Verification code sent to your email" }

For email and sms the code has already been sent — to data.email or data.phone. For authenticator nothing is sent; the user reads it from their app. When the user has no preference set, the method defaults to email.

POST/profile/security/challenge/sendNo auth
POST/profile/security/challenge/verifyNo auth

Both are public — the caller is holding a challengeToken, not a session. Verifying exchanges it for a real token pair.

Setup

GET/profile/security/2fa/statusJWT
POST/profile/security/2fa/setup/:methodJWT
POST/profile/security/2fa/verify-setupJWT
POST/profile/security/2fa/enableJWT
POST/profile/security/2fa/disableJWT

:method is email, sms or authenticator. For authenticator, setup returns the secret and QR payload (otplib); verify-setup proves the user has it working before enable commits.

Backup codes

POST/profile/security/2fa/backup-codesJWT
GET/profile/security/2fa/backup-codes/countJWT

Stored as two_factor_backup records. The count endpoint exists so the UI can nag before the user runs out.

Security settings

GET/profile/security/settingsJWT
PATCH/profile/security/settingsJWT

Org-level switches, also reachable through GET /repository/setting/securitySettings:

SettingEffect
enableTwoFactorForUsersForce 2FA for every user in the org
enableNewDeviceAuthenticationChallenge unrecognized devices even without 2FA
alertOnNewDeviceLoginEmail on first sign-in from a new device. On unless explicitly false.

Passcode behavior lives separately under passcodeLoginSettings.

POS sign-in skips both

POST /profile/signin/passcode bypasses the 2FA and new-device challenges by design — a shared till would otherwise be unusable. Control that surface through the card registry and passcode settings instead.