User Auth

Let visitors register, log in, edit their account and reset a password through your own forms, without ever seeing a WordPress screen.

User Auth is an add-on for Forms. It gives every form you edit a User Auth panel, where you say what the form should do to a user account when it is submitted: create one, log someone in, update a profile, start a password reset, finish one, or send a fresh verification email. A form with one enabled action attached stops being a message you receive and becomes a working account screen on your own pages, styled like the rest of your site.

It also adds one new field — Password — because a login or registration form needs a field that hides what is typed, and Forms does not ship one.

Everything underneath is WordPress's own account machinery. Logging in sets the same session cookie the normal login screen sets, registration creates a normal user, and a password reset uses WordPress's own emailed reset link with its own one-day expiry. This plugin builds the front end; it does not invent a second account system alongside WordPress's.

Forms has to be active

On its own the plugin does nothing at all — no panel, no settings, no Password field. Without XPAC Forms it shows an admin notice asking for Forms and stops there.

Where to find it

Two places, and you normally need both. The per-form part is the User Auth panel in the document sidebar while you edit a form. The site-wide part is Forms Settings → User Auth, which is only a list of five page choices — which of your pages is the login page, the registration page, and so on. Those choices are what make password resets and email verification able to find their way back to your site.

What a form can be made to do

A form may carry several conditional User Auth actions, but one submission may run at most one. If its saved status and conditions make several actions applicable at once, the server refuses that submission before changing an account.

ActionWhat submitting the form doesNeeds the visitor to be
RegistrationCreates a new user account from the answersLogged out
LoginSigns the visitor inLogged out
UpdateSaves changes to the signed-in user's own accountLogged in
Forgot passwordEmails the standard WordPress reset linkEither
Reset passwordSets the new password after that link is followedLogged out
Account verificationSends the verification email againEither

The plugin also looks after the obvious mismatch for you: a login or registration form shown to somebody who is already signed in is replaced with "You are already logged in.", and an update form shown to somebody signed out is replaced with "You need to log in or register."

Setting up a login page

Create a normal Page for logging in and publish it. Draft pages are invisible to the settings below.

Build a form with two fields: one for the username or email, and a Password field. Give both fields a name you will recognise — the panel refers to fields by their label.

In the form's User Auth panel press Add action. Set Choose action to Login.

Under User Account, map each account detail to the form field that supplies it. Login needs two rows: a password, and one of Username, Email or Username or Email. Both rows must point at a field before the modal will let you save.

Press Save, then update the form. Updating the form is what stores the action.

Put the form on your login page, then go to Forms Settings → User Auth and pick that page as the Login page.

A visitor who logs in successfully is sent onward: to the form's own redirect if you set one, and otherwise to your home page. Nobody stays on the login form staring at a success message.

Setting up a registration page

Publish a Page for signing up, and build a form with at least an email field and a Password field.

In User Auth add an action and set Choose action to Registration.

Leave Role alone. Read the warning below before you touch it. Left empty, new accounts get the role from Settings → General → New User Default Role, which is what you want.

Map User Account rows for the email and the password. Add rows for anything else you collect — first name, last name, display name, website, biography.

Optionally add User Meta Fields rows to store answers that are not standard WordPress account details. Here you type the storage name yourself instead of choosing one.

Decide the two toggles: Login user after registration signs the new person in immediately, and Account verification after registration emails them a link they must confirm. If verification is enabled, the account stays signed out until it is confirmed even when both toggles are selected.

Save the action, update the form, place it on the page, and pick that page as the Registration page in Forms Settings → User Auth.

Role assignment is limited by the form author

The Role dropdown is available only to an editor with WordPress's promote_users capability, and then only offers roles whose capabilities that editor already has. The server checks both rules again on every submission. A hand-edited or stale action cannot grant around them. Leave Role empty to use WordPress's New User Default Role for registration and to leave an existing role unchanged on update.

WordPress controls whether registration is open

Registration actions respect Settings → General → Anyone can register. When it is off, the form returns a generic Registration is not open on this site response. An extension can deliberately override this for an invite-only flow with the xpac_forms_user_auth_allow_registration filter.

Settings inside an action

The controls that appear depend on which action you chose.

SettingActionWhat it does
RoleRegistration, UpdateThe role given to the account. The form author must be allowed to promote users and may grant only capabilities they hold; leave it empty for the normal default/no-change behavior.
User AccountallMaps a WordPress account detail (email, username, password, display name, first name, last name, nickname, biography, website) to one of your form fields. Some rows are compulsory and cannot be removed.
User Meta FieldsRegistration, UpdateStores extra answers against the account under a name you type. Can also store a fixed value rather than an answer.
Login user after registrationRegistrationSigns the new person in as soon as the account exists.
Account verification after registrationRegistrationEmails a verification link to the address they registered with.
Login with verified email onlyLoginAdds a per-form verification guard. Pending verification accounts are also refused globally by default when a verification page is configured.
Account verification after updateUpdateEmails a fresh verification link after the account is saved.
Confirm new email addressUpdateHolds an email change until the new address explicitly confirms it. This protection is always enforced when an email field is mapped.
Current password verification (required)UpdateMaps a second Password field containing the existing password. It is required whenever the action changes a password.

Email and password changes are protected on the server

The editor shows the required confirmation mappings, but the protection does not depend on a saved toggle. Every changed email is held until the new address confirms it, and the old address is notified after the commit. Every password change requires the current password. Older forms that saved either option as off are upgraded to the safe behavior at runtime.

The Password field

The Password field behaves like a normal Forms field, with two extras.

OptionWhat it does
Password Visibility ToggleAdds the eye icon that reveals what has been typed.
Password StrengthShows a strength meter as the visitor types, and refuses weak passwords.
Minimum Strength LevelHow strong is strong enough: Insecure (4+ characters), Bad (8+), Good (upper and lower case, 8+), Strong (upper, lower, a number, a symbol, 8+).

It can also be paired with a second Password field as a confirm field, the same way Forms confirms an email address.

Password strength starts at Good

New Password fields enable strength checking at Good by default. Missing or invalid levels in older saved blocks also fall back to Good in both the browser and server, so a malformed setting cannot silently disable enforcement.

Password resets

Forgot-password and reset-password forms drive WordPress's own reset, so the security of the link is WordPress's: it is emailed only to the address already on the account, it stops working after a day, and it stops working once it has been used. Nothing about the link is invented here.

Publish two Pages — one holding a form with a Forgot password action (map Email to an email field), one holding a form with a Reset password action (map Password to a Password field).

In Forms Settings → User Auth pick them as the Forgot password page and the Reset password page.

Check the result by requesting a reset. The link in the email should now point at your reset page instead of the WordPress login screen.

Nominate the reset page, or the emails keep pointing at the WordPress screen

The reset email's link is only rewritten if a Reset password page is chosen in the settings. Build the forms but skip that step and everything looks finished while every reset email still lands the visitor on the WordPress login screen.

Email verification

Verification is explicit and recoverable

Switching on Account verification after registration emails each new account a link. Opening it displays a confirmation step; the visitor must press Verify email address before the token is consumed. Ordinary GET requests from mail scanners and link previews are therefore harmless. The confirmation POST is nonce-protected, atomic and safe to repeat.

Pending accounts are refused by the global WordPress authentication filter, including at wp-login.php and other User Auth login forms. Auto-login is skipped while verification is required. The default verification lifetime is two days and can be changed with xpac_forms_user_auth_token_lifetime.

If mail delivery fails after an account is created, the form reports that the request was accepted but the email was not sent. The account remains blocked, and submitting the same registration again or using an Account verification action retries delivery instead of creating a duplicate account.

Things worth knowing before you launch

Account lookup responses are generic by default

Login, registration, forgot-password and verification-resend flows do not distinguish a known account from an unknown one in their public response. A site that deliberately prefers more specific messages can opt in with the xpac_forms_user_auth_reveal_accounts filter.

There is no limit on attempts

Nothing counts failures. Nobody is locked out, delayed, shown a too many attempts message or asked for a code after a run of wrong passwords, and the same is true of the registration and forgot-password forms. Passwords can be guessed as fast as a script can send requests, and a forgot-password form can be used to send your account holders mail over and over.

Nothing in this plugin fixes that. Insist on strong passwords with the Password field's strength setting, add one of the anti-spam add-ons to the same form (Google reCAPTCHA, Cloudflare Turnstile or Akismet), and if the site holds anything valuable put a rate-limiting service in front of it.

Outbound mail still needs monitoring

The account boundary and retry path are durable, but the plugin cannot guarantee that your host accepts or delivers mail. Test registration, verification, email-change and password-reset messages before launch, and monitor your mail transport in production.

Credentials are write-only

Password and current-password answers are removed before entries, notifications, queued deliveries, pre-submit gates, integration callbacks, exports, rendered answers or public User Auth hooks can receive them. Equal-value copies in ordinary text fields are removed at the same boundary. An accepted auth submission normally keeps a redacted entry and can queue the form's enabled notifications and integrations. Once its durable instance marker is stored, an exact retry is answered without repeating the auth operation. Use Users as the account source of truth and Submissions as the audit and delivery record.

Login redirects stay on this site

Referer and extension-provided redirect values are attribute-escaped and validated as same-host URLs before they are rendered or returned to the browser. Invalid targets fall back to the form's redirect or the site home page.

For developers

Paths are relative to the plugin directory.

Storage

Site-wide page choices live in the Forms settings option, xpac_forms, under keys page_login, page_registration, page_account_verification, page_forgot_password and page_reset_password. Each holds a published Page ID and is exposed as UserAuth::instance()->pages->{login,registration,accountVerification,forgotPassword,resetPassword} (a PluginPage with getValue(), getUrl() and matchesCurrentPage()). Only the login, registration and account-verification pages register a guard callback; the guards run on wp at PHP_INT_MIN + 10 and wp_safe_redirect() away.

Per-form configuration is a userAuth branch of the Forms form_settings post meta, registered into the meta schema and therefore readable and writable over the REST API by anyone who can edit the form:

form_settings.userAuth.items[] = {
  status: bool,
  action: 'registration'|'login'|'update'|'forgotPassword'|'resetPassword'|'accountVerification',
  config: { <action>: { userFields: [{key,value}], userCustomMetas: [{key,value}],
                        userRole: string, loginAfterRegister: bool,
                        accountVerification: bool, verifiedAccount: bool,
                        confirmEmail: bool, confirmPassword: bool } }
}

userFields keys are wp_insert_user()/wp_update_user() field names, plus the login-only pseudo-key username_or_email and the update-only user_current_pass. A value that matches a field block's name takes the submitted answer; anything else is used as a literal, except for credentials: only a Password block may map to user_pass or user_current_pass, and a Password block cannot map to profile data or user meta. userRole is accepted only when the form author has promote_users and already holds every capability in the target role. The editor and submission handler enforce the same rule.

Account verification adds one table, {$wpdb->prefix}xp_signups (id, user_id with a UNIQUE key, token, activated, created, updated), created by maybe_create_table() on xpac_forms_user_auth_activation and pruned on deleted_user. New tokens are random values whose HMAC is stored with a v2: prefix; pending legacy plain-token rows remain readable until used or replaced. Lookups are exact and case-sensitive. The lifetime filter defaults to 2 * DAY_IN_SECONDS, and activation is an atomic conditional update.

Email changes on the Update action use one user meta key, _new_email, holding the HMAC, token version, proposed address and creation time. A GET with ?newuseremail= renders a confirmation form; only its nonce-protected POST commits the change. New requests expire after one day by default and pending legacy metadata remains compatible. Password resets continue to use core's retrieve_password(), check_password_reset_key() and wp_update_user() flow.

Blocks and assets

One block, xp/form-field-password, registered from dist/packages/user-auth/blocks/password and then handed to Blocks::registerFormBlock() with is_changeable, confirmable_as: 'password' and the rules Required and UserAuth\Core\Validation (passwordStrength). Strength checking is enabled at good by default. PHP and browser validation both normalize a missing or invalid legacy minStrengthLevel to good and fail closed.

Three handles: xpac-user-auth-settings (script and style, registered on wp_loaded and injected into the form editor bundle through xpac_forms_post_script_dependencies and xpac_forms_post_style_dependencies) and xpac-user-auth-front, enqueued from xpac_forms_after_form_begin for login forms and on the activation landing state. The editor panel is contributed from JavaScript under settings key userAuth; its role list contains only roles the current editor may grant.

Request path

No REST routes of its own. Everything runs inside Forms' public submit route,

POST /wp-json/xpac/v1/form/<form-id>/<instance>/submit

whose permission_callback is __return_true and which verifies no nonce. User Auth registers runActions() on xpac_forms_pre_submit_callbacks as an applicable mutation, so Forms runs reversible pre-submit gates first. Forms refuses multiple applicable mutations before invoking any of them, and User Auth independently refuses a form with more than one applicable User Auth action. Returning a WP_Error aborts the submission before entry storage.

Before the auth operation, Forms reserves a durable request guard bound to the exact payload, form configuration and user-auth mutation id. After success, runActions() purges credential answers in a finally block and the guard records field names but never values. Forms then stores the redacted entry, safe delivery payload, frozen named-callback manifest and exact-instance marker together. If that entry write fails, an identical retry resumes only accepted storage and delivery dispatch; it does not rerun authentication, throttling, validation, uploads, pre-submit discovery or gates.

A process that stops while the auth mutation is still uncertain is deliberately not replayed. The same rendered instance returns a fail-closed conflict so an account change cannot be duplicated; investigate it and reload the form only after deciding the account's actual state.

Two consequences follow from core's REST cookie handling rather than from this plugin. A request with no X-WP-Nonce is treated as logged out (rest_cookie_check_errors() calls wp_set_current_user(0)), which is what stops an update action being driven cross-site; and login, registration, forgot-password and verification-resend need no nonce at all because they run for anonymous visitors. Add anti-spam and infrastructure rate limiting to public auth forms.

Sessions are WordPress's: wp_signon() for login, and wp_clear_auth_cookie() + wp_set_current_user() + wp_set_auth_cookie() for login after registration. Nothing here issues a token or cookie of its own. When an account-verification page is configured, the verification guard attaches to WordPress's global authenticate filter by default. The xpac_forms_user_auth_enforce_verification filter can deliberately disable that global policy.

Hooks

Public User Auth actions preserve their hook names, argument counts and argument types. Their user-data arrays omit user_pass and user_current_pass; Submission arguments are callback-safe clones with password and confirmation answers removed; and WP_User arguments are clones with user_pass and user_activation_key blanked. This is a deliberate security contract: listeners should load non-secret state by user ID and must not depend on submitted credentials. Referer and redirect hook results are same-host validated at the sink.

On the listening side it attaches to twelve Forms hooks (xpac_forms_init, xpac_forms_post_meta_schema, xpac_forms_post_meta_default_values, xpac_forms_post_script_dependencies, xpac_forms_post_style_dependencies, xpac_forms_editor_inline_js_data, xpac_forms_pre_submit_callbacks, xpac_forms_after_form_begin, xpac_forms_pre_render_form, xpac_forms_render_form_content, xpac_forms_redirect_config, xpac_forms_submit_response) and to core's plugins_loaded, template_redirect (three modules), authenticate, deleted_user, lostpassword_url, retrieve_password_message, render_block_xp/form and render_block_xp/form-field-email.

The generated hook reference below is the source of truth for signatures and call sites.

On this page