Developer notes
Overview · Installation · Configuration · Usage · FAQ
Standards
Section titled “Standards”- TOTP: RFC 6238 on top of HOTP (RFC 4226): HMAC-SHA-1, 6 digits, 30-second steps, the previous, current and next step accepted. Codes are compared with
hash_equalsfor every candidate step (no early exit). Replay protection: the accepted step is stored per method and a code of the same or an older step is refused (atomicUPDATE ... WHERE last_step < step). Secrets: 160 random bits, base32, encrypted with AES-256-CBC + HMAC-SHA-256 (encrypt-then-MAC) under keys derived from the store’s crypt key; during setup the secret lives encrypted in the session until the first code is confirmed. The QR code is drawn as inline SVG by a small encoder in the add-on (byte mode, level M), so theotpauth://URI never leaves the server. - WebAuthn Level 2 (W3C), pure PHP + OpenSSL: own CBOR decoder, COSE keys ES256 (-7, P-256) and RS256 (-257, RSA ≥ 2048 bits) converted to SubjectPublicKeyInfo and checked with
openssl_verify.- Registration:
clientDataJSONtypewebauthn.create, challenge, origin, no cross-origin;rpIdHash; user present; user verified when required; attested credential data; allowed algorithm; credential id unique. Attestation conveyancenone: formatnoneaccepted,packedverified (self attestation and x5c), other formats accepted without trust. The add-on makes no trust decision from attestation. - Assertion: type
webauthn.get, challenge, origin,rpIdHash, user present, user verified (always for passwordless sign-in), signature overauthenticatorData || SHA-256(clientDataJSON), signature counter (refused when a non-zero counter does not grow; setting), backup flags consistent. Passwordless sign-in also requires the user handle and checks it against the account. - Challenges: 32 random bytes, stored as SHA-256 in
?:kv_ls2fa_challengeswith purpose (reg,auth,login), user, a keyed hash of the session id and the relying party id; valid 5 minutes; taken by an atomicDELETE(single use). - Relying party id = request host without port; expected origin = scheme://host[:port] of the request.
- Registration:
- Recovery codes: 10 × 10 characters of the Crockford base32 alphabet (50 bits each), stored as HMAC-SHA-256, used by an atomic update.
O,IandLtyped by the user are read as0,1,1. - Trusted devices: HttpOnly, SameSite=Lax cookie per area (
kv_ls2fa_td_c|a|v) holding a selector and a 256-bit validator; only the HMAC of the validator is stored.
Sign-in flow
Section titled “Sign-in flow”Every CS-Cart sign-in ends in fn_login_user(). Its login_user_pre hook calls Gate::intercept():
- Skipped for: no user id, console, the Storefront REST API (
API), the remember-me cookie re-login (LOGGED_VIA_COOKIE; that cookie is only written after the second step), inactive users, policy off, the emergency switch (admin area only), a valid trusted-device cookie. - Enrolled user: the sign-in is parked in the session (
kv_ls2fa: user, area, panel, mode, time, remember me, return URL, guest order ids), the session id is regenerated,remember_meis removed from the request and the browser is redirected to the second step (force_redirectionfor AJAX requests). The user is not signed in. - Not enrolled and required: the same with mode setup (skipped for accounts created in the last 2 minutes and while the license is not valid).
- After the second step
Gate::complete()runsfn_login_user($id, true)with a bypass flag, assigns guest orders, applies remember me, issues the trusted-device cookie, logs and redirects to the original return URL (own hosts only).
Second-step failures count per account; at the limit the account’s second step is locked and the parked sign-in dropped. Passwordless passkey failures are rate-limited per IP (20 per 15 minutes).
Full page cache (4.21)
Section titled “Full page cache (4.21)”The storefront sign-in form only gets a static passkey button (settings and license; controllers/frontend/init.post.php never reads the session). All per-visitor work runs in the non-cacheable kv_login_security_2fa.* dispatches (pages and POST/XHR endpoints). No ?:storage_data writes; own tables only.
Endpoints
Section titled “Endpoints”| Dispatch | Area | Purpose |
|---|---|---|
kv_login_security_2fa.challenge / auth.kv_ls2fa |
storefront / panel | Second-step page (challenge, setup, recovery codes) |
…verify, …pk_options, …pk_verify, …setup_totp, …setup_confirm, …setup_pk_options, …setup_pk_add, …finish, …cancel |
both (auth.kv_ls2fa_* in the panel) |
Second-step actions (POST, JSON for the passkey calls) |
…login_options, …login |
both | Passwordless passkey sign-in (JSON; storefront CSRF-exempt, protected by the session-bound single-use challenge and the signature) |
kv_login_security_2fa.manage / .security |
storefront / panel | Own security page; actions totp_start, totp_confirm, totp_cancel, m_pk_options, pk_add, pk_rename, remove, codes, devices_revoke |
kv_login_security_2fa.manage, .log, .reset, .unlock |
admin | Overview, security log, reset and unlock (privileges View users / Manage users) |
Data model
Section titled “Data model”?:kv_ls2fa_users (user handle, failure counter, lock) · ?:kv_ls2fa_factors (type T/P, name, encrypted TOTP secret and last step, or credential id, public key PEM, algorithm, counter, relying party id, AAGUID, transports, flags) · ?:kv_ls2fa_recovery · ?:kv_ls2fa_devices · ?:kv_ls2fa_challenges · ?:kv_ls2fa_rate · ?:kv_ls2fa_log (event, user, user type, area, method, keyed IP hash, detail). Deleting a user removes its rows.
PHP: login_user_pre (second step), post_delete_user (rows of the user), user_logout_after (drops a parked sign-in). Templates (storefront): index:login_buttons (.post), profiles:my_account_menu (.post), profiles:account_update (.post), index:scripts, index:styles; (admin): auth:login_form (.post), profiles:tabs_content (.post), profiles:update_buttons (.post), index:scripts, index:styles, addons:tabs_content, addons:manage. Pre/post controllers: frontend init.post; backend auth.pre, profiles.post, addons.post. Test seam: KV_LS2FA_MAIL_CAPTURE (config.local.php only) writes the security e-mails to a file instead of sending them.
License check
Section titled “License check”Tygh\Addons\KvLoginSecurity2fa\License\Client (generated) verifies the license. Gate points: Config::canEnroll() for new authenticator apps and passkeys, the forced setup and the passwordless buttons. Never gated: the second step of enrolled users, recovery codes, removing methods, admin reset and unlock.
Not in 1.0.0
Section titled “Not in 1.0.0”SMS or e-mail codes as a second step (use Phone OTP Login for SMS sign-in), WebAuthn conditional UI (autofill), attestation trust policies (allowed authenticator models), the Ed25519 (-8) algorithm.
File map
Section titled “File map”| Path | Purpose |
|---|---|
app/addons/kv_login_security_2fa/addon.xml |
Settings, tables, e-mail templates |
init.php, func.php |
Hooks |
Tygh/Addons/KvLoginSecurity2fa/Gate.php |
Interception, parked sign-in, completion, trusted devices, recovery codes |
Http.php, Account.php, Admin.php |
Request handling, self-service actions, admin data |
Passkeys.php, WebAuthn/ |
WebAuthn options and ceremonies; Verifier, Cbor, CoseKey |
Totp.php, QrCode.php, Util.php |
TOTP, QR encoder, encryption, cookies, URLs |
Store.php, Log.php, Notifier.php, Config.php |
Data, security log, e-mails, settings |
License/, controllers/backend/kv_login_security_2fa_license.php |
License check (generated) |
Upgrade/Synchronizer.php |
Self-healing upgrades (generated) |
controllers/frontend/, controllers/backend/ |
Pages and endpoints |
schemas/ |
Menu, permissions (admin and vendor), CSRF validation |
var/themes_repository/responsive/templates/addons/kv_login_security_2fa/ |
Second-step page, security page, sign-in button, account links |
design/backend/templates/addons/kv_login_security_2fa/ |
Panel second step, security page, overview, log, profile tab |
js/addons/kv_login_security_2fa/func.js |
Passkey ceremonies (navigator.credentials), copy and print of codes |