Skip to content

Developer notes

Overview · Installation · Configuration · Usage · FAQ

  1. send normalizes the number to E.164 (Phone::normalize(), default calling code from the country select or the settings), checks the allowed countries and the rate limits, creates a challenge and sends the code through the providers of the delivery setting (WhatsApp first, SMS as fallback when both are selected).
  2. The challenge is bound to the visitor’s session: a random 32-byte token lives in the session, the database keeps only its SHA-256. The code is stored as HMAC-SHA256 (token hash + code, keyed with a secret derived from the store’s crypt_key) and compared with hash_equals().
  3. verify_code counts wrong attempts (the challenge dies at the limit), checks expiry, then looks up the account: the verified number first (?:kv_pol_phones), then (setting) numbers typed in customer profiles, normalized in PHP. Exactly one active customer → fn_login_user($user_id, true) (the same call as the core e-mail one-time password: new session ID, cart merge, login hooks). None → registration step (if allowed). Several → refused.
  4. register is accepted for 15 minutes after a confirmed code, once. The account is created with fn_update_user() (random password, the number as profile phone, e-mail notification only when an e-mail was given) and signed in.

Answers before a valid code never depend on whether an account exists. All endpoints are POST + AJAX and CSRF-checked (schemas/security/csrf_validation.post.php). Return URLs are limited to the store’s own hosts.

Table Content
?:kv_pol_phones user_id (PK), phone (E.164), verified_at
?:kv_pol_challenges token hash, purpose (L sign-in/registration, V profile verification), number, user, storefront, code hash, attempts, sends, channel, created/expires/last sent/verified times, status (P pending, V verified, U used, X void). Purged one day after expiry.
?:kv_pol_log time, event, keyed hash and masked form of the number, IP, user, storefront, channel, provider, purpose, message. Rate limits count rows of this table. Purged after the retention period.
Area Dispatch Purpose
Storefront kv_phone_otp_login.login (GET) Sign-in / registration page (return_url supported)
Storefront kv_phone_otp_login.verify (GET) Profile verification page (signed-in customers)
Storefront kv_phone_otp_login.send, .resend, .verify_code, .register (POST, AJAX) JSON answer in kv_pol: ok, step (phone, code, register, done), error + message, redirect_url
Storefront init.post.php, profiles.post.php Assign the kv_pol flags (same for every visitor) and the profile panel data
Admin kv_phone_otp_login.log (GET), .test_send (POST) Sign-in log, provider test. Privileges: view_users / manage_users.

Template hooks (responsive theme): index:login_buttons (post: the button), profiles:account_update (post: profile panel / registration link).

PHP hooks: update_profile (drops the verified record when the profile phone changes to another number), post_delete_user (removes the record). Both have the same signature in 4.17.2 and 4.21.2.

Messaging\ProviderInterface with TestProvider, TwilioProvider (POST /2010-04-01/Accounts/{sid}/Messages.json), GenericHttpProvider and WhatsAppCloudProvider (POST /{version}/{phone-number-id}/messages, template with a body parameter and, optionally, a URL button parameter). Providers never throw; they return a Result (ok, transient, HTTP status, message ID, error without credentials). Messaging\Http uses cURL with 5 s connect / 10 s total timeouts. For local development a mock base URL can be set with the constant KV_PHONE_OTP_LOGIN_API_BASE (never in production). One-time codes are not queued: a code is sent at once, and the fallback channel is the retry.

The button under the login forms depends only on the settings and the license, so cached pages are the same for every visitor. The phone pages are not cacheable dispatches, and every state change is an AJAX POST. Nothing is written on storefront GET requests.

Tygh\Addons\KvPhoneOtpLogin\License\Client (generated) verifies the license. Gate points: Config::isActive() (button and profile panel output, the phone pages, send and resend). Not gated: verify_code and register of a code that was already sent, so a started sign-in can always be finished.

Two-factor authentication for administrators or vendors, phone verification required at checkout, numbers of billing/shipping profiles, voice calls, admin editing of a customer’s verified number.

Path Purpose
app/addons/kv_phone_otp_login/addon.xml Settings, tables
init.php, func.php Hooks, settings variants (countries), storefront flags, fn_kv_phone_otp_login_is_licensed()
Tygh/Addons/KvPhoneOtpLogin/Service.php The code flow: send, resend, verify, register, admin test send
Accounts.php Account lookup by number, verified numbers, registration
Config.php, Phone.php, Log.php Settings with limits, E.164 and calling codes, log and rate-limit counters
Messaging/ Providers, HTTP client, results
License/, controllers/backend/kv_phone_otp_login_license.php License check (generated)
Upgrade/Synchronizer.php Self-healing upgrades (generated)
controllers/frontend/, controllers/backend/ Pages and endpoints
schemas/ Menu, permissions, CSRF validation
var/themes_repository/responsive/templates/addons/kv_phone_otp_login/ Widget, page and hook templates
js/addons/kv_phone_otp_login/func.js The storefront widget (jQuery, no external libraries)