Skip to content

Add native passkey login with email OTP fallback #1357

@superdav42

Description

@superdav42

Summary

Implement built-in passwordless authentication in Ultimate Multisite using an email-first flow with native WebAuthn/passkey support and email OTP fallback.

This should live in Ultimate Multisite itself. Do not depend on the secure-passkeys WordPress plugin at runtime. Its WebAuthn implementation can be used as a reference for challenge generation, browser APIs, credential persistence, and assertion/attestation verification, but Ultimate Multisite should ship only the focused pieces needed for this product flow.

Desired user flow

Login

  1. User sees a login form with email only.
  2. User submits their email.
  3. Ultimate Multisite checks whether the matching user has at least one registered passkey.
  4. If a passkey exists:
    • stay on the same login page/form;
    • prompt the browser passkey ceremony using navigator.credentials.get();
    • complete login on successful WebAuthn assertion.
  5. If no passkey exists:
    • send a short-lived email OTP/code;
    • stay on the same login page/form;
    • prompt for the code;
    • complete login on successful OTP verification.
  6. After the first successful OTP login, prompt the user to create a passkey on the login page using navigator.credentials.create().
  7. Passkey registration must not require visiting the WordPress user profile screen.

Surfaces that must support the flow

  • Default wp-login.php.
  • Ultimate Multisite custom login page when enable_custom_login_page is enabled.
  • Ultimate Multisite login block/shortcode/element rendered by WP_Ultimo\UI\Login_Form_Element.
  • Checkout inline login prompt shown when an existing email/username is entered during checkout/signup.

Current relevant code

Use these files as the primary integration points:

  • inc/class-wp-ultimo.php
    • Registers singleton services including WP_Ultimo\UI\Login_Form_Element and SSO classes.
    • Add the new passwordless/passkey service here once implemented.
  • inc/ui/class-login-form-element.php
    • Current login form element builds username/password fields around lines ~777-845.
    • Current form posts to site_url('wp-login.php', 'login_post') around lines ~876-889.
    • Existing reset/lost-password behaviour must keep working unless intentionally replaced by the new passwordless flow.
  • views/dashboard-widgets/login-form.php
    • Renders the login form element wrapper and title.
  • views/checkout/partials/inline-login-prompt.php
    • Currently asks for a password for existing users during checkout/signup.
    • Replace/extend this with the email-first passkey/OTP flow.
  • inc/checkout/class-checkout-pages.php
    • Owns custom login page routing and wp-login.php URL replacement.
    • Preserve custom-login-page URL behaviour, especially subsite-domain password/reset link handling.
  • inc/sso/class-sso.php and inc/sso/*
    • Preserve existing multisite SSO behaviour and redirect-loop protections.
    • Ensure successful passkey/OTP login uses the same redirect/SSO semantics as current password login.
  • Existing tests to update/extend:
    • tests/e2e/cypress/integration/login.spec.js
    • tests/e2e/cypress/support/commands/login.js
    • tests/WP_Ultimo/Checkout/Checkout_Pages_Test.php
    • tests/WP_Ultimo/SSO/SSO_Test.php

Suggested implementation shape

Create a focused authentication subsystem, for example:

  • inc/auth/class-passwordless-auth.php or inc/managers/class-passwordless-auth-manager.php
  • inc/auth/class-passkey-service.php
  • inc/auth/class-email-otp-service.php
  • inc/auth/class-webauthn-challenge-store.php
  • inc/auth/class-passkey-credential-store.php

The exact structure is flexible, but keep the feature cohesive and avoid mixing WebAuthn protocol logic directly into UI templates.

Data storage

Add minimal storage for:

  • WebAuthn credentials:
    • user ID
    • credential ID / raw ID
    • public key
    • sign count/counter
    • AAGUID if available
    • created/updated/last-used timestamps
    • active flag
  • WebAuthn challenges:
    • challenge value
    • type: registration or authentication
    • optional user ID
    • expiration timestamp
    • used timestamp/status
  • Email OTP attempts:
    • store hashed OTP only, never plaintext;
    • user ID/email association;
    • expiration timestamp;
    • attempt count;
    • consumed timestamp/status.

Use Ultimate Multisite's existing database/table conventions where practical ({$wpdb->prefix}wu_*, installer/migrator patterns, and tests for table creation). Avoid adding large admin/activity-log surfaces in this first pass.

WebAuthn requirements

Implement passkey registration and authentication directly in Ultimate Multisite:

  • Generate registration options for a specific authenticated/OTP-verified user.
  • Verify registration attestation response and store credential data.
  • Generate authentication options scoped to the entered email/user when possible.
  • Verify assertion response, credential ID, challenge, origin/RP ID, signature, and sign counter.
  • Use HTTPS-origin-safe logic and multisite/domain-mapping-safe RP ID selection.
  • Use WordPress nonces on AJAX/REST endpoints.
  • Use wp_set_auth_cookie(), wp_set_current_user(), and standard login hooks so SSO, redirects, and audit hooks continue to work.

Reference only: the WordPress.org secure-passkeys plugin implements a similar split with frontend AJAX actions for login options/login/register options/register passkey and JS using navigator.credentials.get() / navigator.credentials.create(). Do not copy its admin overview, activity log, profile-page UX, or settings pages unless a small piece is actually needed.

Email OTP requirements

  • Code should be short-lived, single-use, and rate-limited.
  • Store only a hash of the code.
  • Apply rate limits by user/email and IP to prevent brute force and mail flooding.
  • Return generic responses where practical to reduce email enumeration.
  • Email should use Ultimate Multisite/WordPress mail patterns and be translatable with text domain ultimate-multisite.
  • On successful OTP login, immediately show/passkey-enrol prompt on the same page.

UI/UX requirements

The login UI should be a progressive state machine on one page:

  1. Email entry.
  2. Passkey prompt if available, otherwise OTP prompt.
  3. OTP verification.
  4. First-OTP-login passkey enrolment prompt.
  5. Completion/redirect.

Do not send users to /wp-admin/profile.php to register a passkey.

Keep copy using the product name “Ultimate Multisite” where product naming is needed. Preserve the WP_Ultimo namespace and wu_ hooks/prefixes.

Backward compatibility and migration notes

  • Existing username/password login may remain as an emergency/admin fallback if needed, but the customer-facing Ultimate Multisite login surfaces should default to email-first passwordless auth.
  • Existing custom login page behaviour and wp-login.php URL rewrite tests must remain valid.
  • Existing SSO redirect-loop guards must remain valid.
  • Existing reset-password flows should not break unexpectedly; if reset password becomes secondary, keep compatibility for existing links and tests.
  • The old two-factor assumptions should be removed from any Ultimate Multisite/passwordless integration work; this feature must not require the WordPress Two-Factor plugin.

Security considerations

  • Auth feature: treat as security-sensitive.
  • Never log OTPs, raw WebAuthn challenges, credential private data, or email codes.
  • Validate all AJAX/REST requests with nonces and capabilities/context checks.
  • Sanitize all request values with WordPress helpers.
  • Escape all UI output.
  • Avoid account enumeration where possible.
  • Add brute-force and resend throttling for OTP and passkey challenge attempts.
  • Ensure passkey challenges are single-use and expire quickly.
  • Ensure authentication cookies are set only after verified passkey assertion or OTP.

Acceptance criteria

  • Ultimate Multisite ships native WebAuthn/passkey registration and authentication without requiring the secure-passkeys plugin.
  • Login form asks for email first, not password first.
  • Existing users with a passkey are prompted for passkey login after email entry.
  • Existing users without a passkey receive and can verify an email OTP.
  • After first successful OTP login, users can create a passkey on the login page.
  • wp-login.php supports the new flow.
  • Ultimate Multisite custom login page supports the new flow.
  • WP_Ultimo\UI\Login_Form_Element login block/shortcode supports the new flow.
  • Checkout inline login prompt supports the new flow instead of requiring a password.
  • Existing custom login page URL rewriting and subsite-domain reset-link behaviour remains covered.
  • Existing SSO login/redirect-loop protections remain covered.
  • OTPs are hashed, short-lived, single-use, and rate-limited.
  • WebAuthn challenges are short-lived, single-use, and validated for type/user/origin/RP ID.
  • No new broad admin/activity-log UI is added in this first implementation.

Suggested tests / verification

PHP/unit/integration

Add or update focused tests for:

  • passkey credential/challenge storage and expiry;
  • OTP generation, hashing, expiry, consumption, and attempt limits;
  • login redirect behaviour after passkey and OTP success;
  • custom login page compatibility in Checkout_Pages;
  • SSO compatibility where login originates from a subsite/domain-mapped site.

Likely commands:

vendor/bin/phpunit --filter Login_Form_Element
vendor/bin/phpunit --filter Checkout_Pages_Test
vendor/bin/phpunit --filter SSO_Test
vendor/bin/phpunit --filter Passwordless
vendor/bin/phpcs inc/auth tests/WP_Ultimo
vendor/bin/phpstan analyse

Use exact filters/classes created by the implementation; do not run non-existent filters.

E2E

Update Cypress login coverage:

  • tests/e2e/cypress/integration/login.spec.js
  • tests/e2e/cypress/support/commands/login.js

Scenarios:

  • email-only login form appears;
  • OTP fallback flow can be completed using a captured/test email code;
  • passkey-supported browser path is wired and gracefully skipped/mocked where WebAuthn cannot run in CI;
  • checkout inline login no longer requires a password field.

Manual local verification

  • Activate Ultimate Multisite in the local multisite dev environment.
  • Enable custom login page.
  • Verify /wp-login.php, the configured custom login page, and checkout inline login all show the email-first flow.
  • Create/log in a user without passkey via OTP and register a passkey on the login page.
  • Log out and confirm the same email now routes to passkey login.
  • Confirm failed/expired OTP and cancelled passkey ceremonies show safe errors without logging the user in.

Notes for implementer

  • Keep implementation PHP 7.4 compatible.
  • Follow WordPress Coding Standards used by this repo.
  • Public/base class method signatures should not add return type declarations where addon compatibility could be affected.
  • Use relative repo paths only in PR descriptions/comments.
  • Include migration/schema changes and rollback-safe tests in the PR.

aidevops.sh v3.20.41 plugin for OpenCode v1.16.2 with gpt-5.5

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestorigin:interactiveCreated by interactive user sessionpriority:highHigh severity — significant quality issuesecuritySecurity-sensitive issue or changestatus:in-reviewPR open, awaiting review/mergetier:2~1-2 days, multi-file

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions