Lewati ke konten utama

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_devices and auth.device_login_challenges are in place
  • resolve-phone already accepts device_id, device_name, and platform_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
  • the cancel-new-device-login endpoint is available
  • the active endpoints still use the main OTP flow:
    • POST /auth/resolve-phone
    • POST /auth/request-otp
    • POST /auth/verify-otp
    • GET /auth/device-login/cancel
  • the request-device-verification and verify-device-otp routes 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.go
  • internal/service/device.go
  • internal/service/otp.go
  • internal/store/auth_audit.go
  • internal/transport/http/routes.go
  • internal/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_required state
  • 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:

  • phone
  • device_id
  • device_name
  • platform_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:

  1. verified Gmail
  2. verified WhatsApp
  3. 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

The security notification must provide a cancel action:

  • Email: Secure Account or Cancel This Login button
  • WhatsApp: link with a signed token

If the user takes that action:

  • the new session is revoked
  • the new device is marked revoked or blocked
  • 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_verification
  • trusted
  • revoked
  • blocked

Meaning:

  • pending_verification: a new-device challenge has been created but has not yet passed OTP
  • trusted: the device has passed verification and may be used for subsequent logins
  • revoked: the device session was previously cancelled by the user
  • blocked: the device is blocked and may not directly request a bypass

Required Tables

auth.user_devices

Minimum columns:

  • id
  • user_id
  • platform_code
  • device_id
  • device_name
  • status
  • trusted_at
  • revoked_at
  • blocked_at
  • last_seen_at
  • created_at
  • updated_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:

  • id
  • user_id
  • device_id
  • platform_code
  • channel
  • destination_masked
  • purpose
  • status
  • otp_request_id
  • cancel_token_hash
  • cancel_expires_at
  • approved_at
  • cancelled_at
  • created_at

auth.auth_audit_logs

More specific events should be added:

  • resolve_phone_existing_user
  • resolve_phone_new_device_detected
  • device_login_otp_requested
  • device_login_otp_verified
  • device_login_challenge_cancelled
  • device_marked_trusted
  • security_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-phone result
  • still use the request body that carries device_id, device_name, and platform_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_email or a verified email is available
  • security notification with a Secure Account button

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_devices migration
  • create the auth.device_login_challenges migration
  • add a clear device enum/status
  • make resolve-phone device-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_id and device_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