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.
| Action | What submitting the form does | Needs the visitor to be |
|---|---|---|
| Registration | Creates a new user account from the answers | Logged out |
| Login | Signs the visitor in | Logged out |
| Update | Saves changes to the signed-in user's own account | Logged in |
| Forgot password | Emails the standard WordPress reset link | Either |
| Reset password | Sets the new password after that link is followed | Logged out |
| Account verification | Sends the verification email again | Either |
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.
| Setting | Action | What it does |
|---|---|---|
| Role | Registration, Update | The 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 Account | all | Maps 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 Fields | Registration, Update | Stores extra answers against the account under a name you type. Can also store a fixed value rather than an answer. |
| Login user after registration | Registration | Signs the new person in as soon as the account exists. |
| Account verification after registration | Registration | Emails a verification link to the address they registered with. |
| Login with verified email only | Login | Adds a per-form verification guard. Pending verification accounts are also refused globally by default when a verification page is configured. |
| Account verification after update | Update | Emails a fresh verification link after the account is saved. |
| Confirm new email address | Update | Holds 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) | Update | Maps 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.
| Option | What it does |
|---|---|
| Password Visibility Toggle | Adds the eye icon that reveals what has been typed. |
| Password Strength | Shows a strength meter as the visitor types, and refuses weak passwords. |
| Minimum Strength Level | How 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>/submitwhose 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.
TouchBasePro
Add the people who submit your forms to a TouchBasePro subscriber list, filling the list custom fields from the answers you mapped.
User Targeting
Add visitor conditions to XPAC Popups and XPAC Forms, so a popup opens or a form section appears only for visitors matching the device, page address, cookie, browser storage value or country you choose.