Device Verification Login Architecture
This document describes the target login flow for Kesles Merchant that hardens security when the same number is used on different devices.
Current Implementation Status
Status as of 8 April 2026:
- migrations for
auth.user_devicesandauth.device_login_challengesare in place resolve-phonealready acceptsdevice_id,device_name, andplatform_code- a new device may no longer issue a token directly
- the new-device verification channel selects:
- verified Gmail when available
- WhatsApp if no verified Gmail exists
- after a successful verify, the backend now:
- marks the device as
trusted - persists the session with
device_id - generates a cancel token
- sends a security notification to Gmail or WhatsApp
- marks the device as
- the cancel-new-device-login endpoint is available
- the active endpoints still use the main OTP flow:
POST /auth/resolve-phonePOST /auth/request-otpPOST /auth/verify-otpGET /auth/device-login/cancel
- the
request-device-verificationandverify-device-otproutes are not used as separate endpoints
Notes:
- this implementation is already active for mobile login during development
- the email security notification uses the final HTML template
- the WhatsApp security notification is still a text message + link
- the cancel link currently still returns JSON, not the final web page
- this flow can still be polished for final production, but it is no longer just an architectural draft
The main backend files handling this flow (in services/auth_service):
internal/store/auth_devices.gointernal/service/device.gointernal/service/otp.gointernal/store/auth_audit.gointernal/transport/http/routes.gointernal/app/server.go
Implementation Cleanliness Principles
During the development phase:
- temporary fields, endpoints, and flows that are not used must be removed
- do not let columns or response fields linger as experimental leftovers
- every new field must have a clear purpose in the backend, mobile, or audit
- if a field is not used across layers, remove it before stabilization
Current State
The login flow currently in place for a WhatsApp number already known to the system is:
- a registered number does not auto-login from a new device
- a new device enters the
verification_requiredstate - the OTP channel is determined by the available verified identity:
- verified Gmail first
- WhatsApp if no verified Gmail exists
- after OTP success:
- a new session is created
- the device is marked
trusted - a cancel token is created
- a security notification is sent
- the new session can be cancelled within a defined window
- important events are written to the audit log
Core Rules
1. Resolve Phone Must Be Device-Aware
POST /auth/resolve-phone may no longer answer based solely on whether the number exists in the database.
The endpoint must consider:
phonedevice_iddevice_nameplatform_code- whether the device is already trusted
- whether there is a still-valid active session for that device
2. A New Device Always Requires OTP
If the number exists but the device_id is not known:
- do not issue an access token directly
- do not bypass OTP
- the status must move to a verification challenge
3. OTP Channel Priority
For a new device:
- if the user has a verified Gmail, send OTP to Gmail
- if there is no verified Gmail, send OTP to WhatsApp
- SMS is only an optional fallback, not the main channel
Initial priority:
- verified Gmail
- verified WhatsApp
- optional SMS
4. Security Notification After Successful Login
After a new-device OTP success:
- a new session is created
- the device is marked trusted
- a security notification is sent
Channel rules:
- if a verified Gmail is available, send to Gmail
- if no Gmail is available, send to WhatsApp
- SMS is not used for successful login notifications
5. Cancel Link / Secure Account Action
The security notification must provide a cancel action:
- Email:
Secure AccountorCancel This Loginbutton - WhatsApp: link with a signed token
If the user takes that action:
- the new session is revoked
- the new device is marked
revokedorblocked - the cancellation event is written to the audit log
6. Timeout Window
If the user does not cancel within the defined window:
- the new session stays active
- the status is not treated as explicit approval
- the status only means there was no cancellation within the given window
Initial recommendation:
- cancellation window:
10 minutes
Device Status
The device table should know the following statuses:
pending_verificationtrustedrevokedblocked
Meaning:
pending_verification: a new-device challenge has been created but has not yet passed OTPtrusted: the device has passed verification and may be used for subsequent loginsrevoked: the device session was previously cancelled by the userblocked: the device is blocked and may not directly request a bypass
Required Tables
auth.user_devices
Minimum columns:
iduser_idplatform_codedevice_iddevice_namestatustrusted_atrevoked_atblocked_atlast_seen_atcreated_atupdated_at
Minimum unique:
(user_id, platform_code, device_id)
auth.device_login_challenges
Used for device-aware challenges beyond the very short-lived Redis OTP.
Minimum columns:
iduser_iddevice_idplatform_codechanneldestination_maskedpurposestatusotp_request_idcancel_token_hashcancel_expires_atapproved_atcancelled_atcreated_at
auth.auth_audit_logs
More specific events should be added:
resolve_phone_existing_userresolve_phone_new_device_detecteddevice_login_otp_requesteddevice_login_otp_verifieddevice_login_challenge_cancelleddevice_marked_trustedsecurity_notification_sent
Target Endpoints
POST /auth/resolve-phone
Minimum request:
{
"phone": "628114169868",
"platform_code": "mobile_app",
"device_id": "android-abc-123",
"device_name": "Infinix XPAD 20"
}
Possible responses:
{
"status": "verification_required",
"reason": "new_device",
"verification_channel": "email"
}
Or for a trusted device:
{
"status": "authenticated",
"access_token": "<jwt>",
"refresh_token": "<opaque_token>"
}
POST /auth/request-otp
Responsibilities in the device-aware flow:
- choose the OTP channel based on the
resolve-phoneresult - still use the request body that carries
device_id,device_name, andplatform_code - create the OTP challenge linked to the device login challenge
- send the OTP to verified Gmail or WhatsApp
- write the audit event
POST /auth/verify-otp
Responsibilities in the device-aware flow:
- verify the new-device OTP
- create a new
user_id-based session - mark the device
trusted - create a cancel token
- send the security notification
- write the audit and update the trusted device state
POST /auth/device-login/cancel
Responsibilities:
- validate the signed cancel token
- revoke the new session
- change the device status
- write the audit event
Channel Integration
Gmail
Used for:
- OTP verification of a new device when
primary_emailor a verified email is available - security notification with a
Secure Accountbutton
WhatsApp
Used for:
- new-device OTP if no verified Gmail exists
- security notification with a cancel link
SMS
Used only when truly needed as an extra fallback.
Implementation Principles
- the device source of truth must live in PostgreSQL
- the very short-lived OTP challenge may still live in Redis
- the cancel link must be single-use
- the cancel token must be signed and have an expiry
- a trusted device does not mean unlimited bypass; it remains tied to session validity
Implementation Checklist
- create the
auth.user_devicesmigration - create the
auth.device_login_challengesmigration - add a clear device enum/status
- make
resolve-phonedevice-aware - always route a new device to OTP
- choose verified Gmail first if available
- fall back to WhatsApp if no Gmail
- send a security notification after a successful device login
- add a cancel link/token
- add the cancel-new-device-login endpoint
- write the core device login events to
auth.auth_audit_logs - update Flutter to always send
device_idanddevice_name - update the OTP UI to show the chosen verification channel
- swap the email security notification from plain text to the final template
- swap the WhatsApp security notification to the final format/template
- change the cancel link to open a more user-friendly web/app page
- audit unused temporary fields/endpoints and remove them before release