Description
U2 Secured Authenticator adds push-based two-factor authentication to WordPress login. After a user’s
password is verified, WordPress holds the session open (it does not complete it) and challenges for a
second factor:
- Tap-to-approve push — a number-match approval sent to the user’s phone via the U2 Secured app.
- TOTP fallback — a standard six-digit authenticator-app code, for when a push can’t reach the
phone. Verified on your own server: the RFC 6238 check runs in PHP against the secret this site
already holds (encrypted at rest), with the same ±1 window tolerance for clock drift, and no request
leaves the site to do it. Requires thesodiumPHP extension and an encryption key (see
Installation); the plugin says so plainly on the profile screen when it isn’t available, rather than
silently dropping the option. Wrong codes are rate-limited per user (five inside fifteen minutes)
so a six-digit code cannot simply be ground through. -
Recovery codes — ten single-use codes issued when a user links their device, verified entirely
locally against WordPress’s own database.Both fallbacks are what make the failure mode of an unreachable U2 API survivable: the push path is
dead, but a linked user can still sign in with either, because neither makes an outbound call. They
share one field on the fallback screen, which accepts whichever the user has to hand. - Role-based enforcement with a grace period — require two-factor for chosen roles (e.g.
Administrator) without locking out every matching user the instant it’s switched on. Each user gets a
configurable number of days to link before they’re actually blocked. - Step-up re-auth — a small set of sensitive admin capabilities (e.g. installing plugins) can
require a fresh tap before they’re usable, even mid-session. - Optional signed webhook — U2 Secured can POST the outcome of an approval straight to this site
the moment the user taps, so the login screen can answer from local state instead of asking again.
It is an optimisation and nothing more: every delivery must carry a valid signature to be believed,
and login works identically on a site that never receives one, because the browser asks U2 Secured
for the outcome itself anyway. Sites that aren’t reachable from the public internet should simply
skip it. - XML-RPC / Application Passwords awareness — XML-RPC authenticates with the raw password and never
reaches thewp_loginhook this plugin relies on, so it’s explicitly closed off. Application
passwords are a legitimate WordPress feature, so instead of being silently broken they’re surfaced to
the administrator as an admin notice.
Every enrolment is opt-in until an administrator turns on role-based enforcement. Every error path —
an unreachable U2 API, a missing encryption key, an unrecognised device, a removed or rotated-out API
key — is designed to fail closed: the safe outcome is “ask again” or “refuse,” never “let the
request through.” A user who requires a second factor on a site that can no longer perform one is
refused and told to ask an administrator; they are not quietly let in. See Verification status in
README.md for exactly what is and isn’t proven by the automated tests, and please read it before
enabling enforcement on a production site.
The escape hatch
If a user loses their phone and their recovery codes, the only way back in is an administrator
running, over SSH:
wp u2auth disable <user>
<user> accepts a WordPress user ID, login, or email address. This clears the plugin's local
two-factor state for that user (it does not touch anything on the U2 side, so it works even when the
U2 API is unreachable) and immediately admits them again — they can re-link from their profile once
back in. wp u2auth status <user> reports whether a user is currently linked and how many recovery
codes they have left, without changing anything.
External services
This plugin cannot work on its own: approving a login means asking a phone, and that request
travels through the U2 Secured Authenticator service at https://auth.u2secured.com. Installing
the plugin does not by itself send anything — nothing leaves your site until an administrator
enters an API key and a user links a phone.
When the site contacts the service
- When a user links their phone: to mint a one-time pairing code, then once every few seconds
until the code is redeemed or expires, and once more to read the resulting link. - When a linked user opens their own profile screen, to read whether their phone is still
linked and able to receive approvals. - When a linked user signs in, to ask their phone to approve it, and then once every two
seconds until they answer or the request expires. - When a linked administrator performs a sensitive action (installing a plugin, editing a
user), for the same approval round trip. - When a user unlinks their phone, to remove the link on the U2 side as well.
- When an administrator presses “Test connection” on the settings screen.
What is sent
- An identifier for the user, in the form
<random site id>|<numeric WordPress user ID>. The
site id is a random string generated once and stored in your database. Your users’ email
addresses, usernames and passwords are never sent, and neither is your site’s URL. - Your site’s name (from Settings General), so the person looking at their phone can see
which site is asking. - For a sensitive admin action, the name of that action — for example “Install a plugin”.
- While a phone is being linked, the one-time pairing code the service itself just issued; while
an approval is outstanding, the id of that approval. Neither is derived from anything about
the user. - Your site’s API key, to authenticate the request.
What is never sent
- Recovery codes and TOTP codes, and the TOTP secret itself. Both fallback factors are
checked inside PHP on your own server — recovery codes against hashes in your database, TOTP
against the secret your site holds (RFC 6238, ±1 window) — so no request is made to check
either, and the shared secret never leaves the site after enrolment. Earlier releases did send
the TOTP secret and the submitted code to the service to be checked; 0.3.0 stopped. - Passwords, usernames, email addresses, and your site’s URL.
Service terms: https://auth.u2secured.com/terms
Privacy policy: https://auth.u2secured.com/privacy
Installation
-
Upload the plugin to
/wp-content/plugins/dollarmates-u2-secured-authenticator(or install the zip through
Plugins Add New), then activate it. -
Create an app in the U2 Secured developer portal and copy its server API key
(rka_...). This key is what lets the plugin request push approvals, mint pairing codes and read
enrolments. It is not used to check TOTP or recovery codes — both are verified on your own server.Keep it in place once users have enrolled. Removing or rotating it out does not switch
two-factor off; it makes the push factor impossible, and a user who requires a second factor is then
refused at login rather than let through. Usewp u2auth disable <user>(below) if you need to
release someone. -
Add the API key to
wp-config.php. Awp-config.phpconstant is preferred over the plugin’s own
settings screen because it isn’t copied into every database backup:define( ‘U2AUTH_API_KEY’, ‘rka_…’ );
(The settings screen at Settings U2 Secured still accepts it as a fallback for sites that can’t
editwp-config.php— but the constant always wins, and the field is disabled on the settings screen
once it’s set.) -
Add a second constant,
U2AUTH_ENCRYPTION_KEY. This one gates a security control: it is the key
used to encrypt the site-held TOTP secret at rest, so that a database dump alone doesn’t hand over
working two-factor secrets alongside the accounts they protect. Without it, the TOTP fallback is
unavailable (the plugin says so on the profile screen and in an admin notice; push and recovery
codes still work) rather than storing the secret unencrypted.define( ‘U2AUTH_ENCRYPTION_KEY’, ‘<32 random bytes, base64>’ );
Generate a value with:
php -r ‘echo base64_encode(random_bytes(32)), PHP_EOL;’
Requires the
sodiumPHP extension, which ships enabled by default on PHP 7.2+. -
Visit Settings U2 Secured. The Getting started panel there repeats these steps and
reports which of them this site has actually completed, and Test connection checks the API key
and base URL against U2 Secured with a single read-only request — so a wrong key is something you
find out by pressing a button, not by being locked out at your next login. The same screen is where
you optionally:- pick which roles are required to use two-factor (leave every role unticked to keep linking opt-in),
- set the grace period, in days, a newly-required user gets before they’re actually blocked.
-
Optional: set up the webhook. In the developer portal, point your application’s webhook at the
URL shown in step 4 of the Getting started panel. Take it from there rather than constructing
it: the URL depends on your site address and your permalink settings, the panel shows the single
form that is correct for your site as it is configured, and a stale URL in the portal fails
silently. On most sites it reads:https://example.com/wp-json/u2auth/v1/webhook
and on a site using plain permalinks,
https://example.com/?rest_route=/u2auth/v1/webhook. If you
change your permalink settings later, come back and copy the new URL across.Then give this site the signing secret the portal shows for that webhook, so it can tell a real
delivery from a forged one:define( ‘U2AUTH_WEBHOOK_SECRET’, ‘…’ );
(Again, the settings screen accepts it as a fallback. An unsigned, wrongly signed, or stale
delivery is rejected, and with no secret configured at all every delivery is rejected — “not
configured” is never read as “accept anything”.)Skip this step entirely if the site isn’t reachable from the public internet — a local install,
an intranet site, anything behind NAT or a VPN. Nothing breaks: the login screen asks U2 Secured
for the approval outcome itself, and always did. All the webhook saves is an outbound call or two. -
Each user links their own device from their own profile screen (Users Profile, or Your
Profile in the admin toolbar) — an administrator cannot link a device on another user’s behalf,
since linking inherently needs that user’s own phone. Linking also issues ten single-use recovery
codes, shown once and never shown again, so tell people to save them somewhere they can reach
without their phone.
FAQ
-
What happens if a user loses their phone?
-
They fall back to a recovery code (issued, ten at a time, when they first linked, and shown again any
time they choose to regenerate them from their profile) or a TOTP code from a backup authenticator app,
if one was set up. If they’ve lost the phone and exhausted their recovery codes, an administrator
runswp u2auth disable <user>over SSH — see “The escape hatch” above. This is a local, offline
operation: it works even if the U2 API itself is unreachable. -
Does turning on role-based enforcement lock everyone out immediately?
-
No. A user in a newly-enforced role gets the configured grace period (default seven days) to link
before they’re blocked — they’re nagged on their profile screen, not refused at login, until that
window closes. -
What happens if the U2 API is unreachable?
-
The plugin fails closed, and there is no setting that changes that — opting out is not offered.
In practice that is less dramatic than it sounds. An already-linked user cannot use the push challenge
while U2 is unreachable, but can still sign in with a recovery code or a TOTP code: both are verified
against this site’s own data with no outbound call at all. The login is still gated, just via a
different factor. If the phone is gone and the codes are exhausted,wp u2auth disable <user>over
SSH is the escape hatch, and it makes no network call either.What the plugin will not do is let a session through with no second factor because a network call
failed. See “Fail-closed means a misconfiguration blocks logins” under Verification status in
README.md for what that means for your own rollout. -
What happens if the API key is removed, or rotated out?
-
Users who require a second factor are refused, with a message telling them to ask an administrator to
check the key. They are not let in.This is worth stating plainly because the alternative is the more tempting behaviour and it is a
two-factor bypass:wp_loginfires after WordPress has already set the auth cookie, so a plugin that
merely declines to challenge when it is unconfigured has thereby completed the login. Up to and
including 0.2.2 this plugin did exactly that, and deleting the API key silently switched two-factor off
for every user who had it. Fixed in 0.3.0.A site with the plugin installed and nobody enrolled is unaffected: it behaves exactly as WordPress
does, which is why the check asks “does this user need a second factor” before it asks “can this site
send one”. -
What stops someone guessing a TOTP code?
-
Five wrong codes for one user inside fifteen minutes, and the TOTP factor refuses that user until the
window passes. Recovery codes are checked before the limit applies, so a user who fumbled their
authenticator app still has their codes — the limit is a brake, never a lockout, and it clears itself.Each guess also costs a full password sign-in, because the challenge is single-use: a failed code
invalidates the attempt and the attacker has to authenticate the password again to get another. -
Do I need the webhook?
-
No. It’s an optimisation, never a requirement. When a user taps Approve, U2 Secured can POST that
outcome to this site immediately; the login screen then answers from local state instead of asking U2
Secured again. Without it the login screen simply asks — which is what it does anyway while it waits.
Every login flow, fallback and failure mode is identical either way.Set it up only if this site is reachable from the public internet. If it isn’t, deliveries would never
arrive and nothing would tell you so, which is worse than not configuring it. The URL to paste into
the developer portal is printed on Settings U2 Secured, under Getting started — take it from
there rather than typing it out, since it follows your site address and your permalink structure, and
that screen already resolves which of the two forms your site actually uses. -
What happens if someone forges a webhook delivery?
-
It’s rejected. Every delivery carries an HMAC signature over the exact bytes sent, computed with the
shared secret fromU2AUTH_WEBHOOK_SECRET(or the settings screen), and is verified before a single
field of the payload is read. A wrong signature, a signature computed over different bytes, a stale
timestamp, or a malformed header are all refused and change nothing. If no secret is configured at
all, every delivery is refused — “not configured” is never treated as “accept anything”. A rejected
webhook can’t strand a login either: the browser’s own poll resolves the approval regardless. -
Does this affect XML-RPC or Application Passwords logins?
-
XML-RPC authenticates with the raw password and never reaches the hook this plugin challenges on, so
the plugin closes it off outright rather than leaving a second, unguarded front door. Application
Passwords are a legitimate WordPress feature with their own use cases, so instead of being blocked
they’re surfaced as an admin notice — decide for your site whether to disable them separately. -
Can an administrator link or unlink a device for another user?
-
No. Linking needs that user’s own phone, so there is no “link on behalf of” flow. The only admin-side
action is thewp u2auth disable/wp u2auth statusescape hatch described above.
Reviews
There are no reviews for this plugin.
Contributors & Developers
“DollarMates U2 Secured Authenticator” is open source software. The following people have contributed to this plugin.
ContributorsTranslate “DollarMates U2 Secured Authenticator” into your language.
Interested in development?
Browse the code, check out the SVN repository, or subscribe to the development log by RSS.
Changelog
0.3.1
- The TOTP attempt limit could be stretched by sending guesses in parallel. The five-per-15-minutes
counter was advanced by reading it, adding one, and writing the result back. Two submissions that
overlapped both read the same number and both wrote the same number, so one of the two guesses cost
nothing — making the limit a function of how many requests were sent at once rather than how many
guesses were made. The counter is now incremented atomically: through the object cache’s own
increment where a persistent cache is available, and otherwise by a single self-referential SQL
UPDATE that the database serialises. Reported by the plugin review team.
0.3.0
- Security: a second factor could be skipped entirely on a site whose API key had been removed or
rotated out. The login interception treated “this user requires a second factor” and “this site can
perform one” as the same condition, and answered both by returning — butwp_loginfires after
WordPress has already set the auth cookie, so returning there completed the login. Any already-linked
user therefore signed in with a password alone once the key was gone, which is the opposite of what
this readme promised. Entitlement is now decided first and on its own: a user who needs a second
factor and cannot be given one is refused, with a message naming the fix. A site with nobody enrolled
still signs users in exactly as WordPress does. - TOTP codes are now verified on your own server, and the TOTP secret is no longer transmitted.
Verification used to POST the shared secret and the submitted code to the U2 Secured API on every
fallback login — while this readme said, in as many words, that TOTP codes are verified on your own
server and never transmitted. The RFC 6238 check (same ±1 drift window, same answer) now runs in PHP
against the secret this site already holds, so nothing goes on the wire, and a U2 outage no longer
takes the TOTP factor down with push. - Wrong TOTP codes are rate-limited locally: five per user inside fifteen minutes. The remote endpoint
applied a brute-force lockout shared across its callers, and moving the check in-house would
otherwise have dropped that protection. Recovery codes are checked before the limit, so it can
never become a lockout, and a throttled user is told they are throttled rather than told their code
was wrong. - The fallback screen now says that an authenticator-app code is accepted there too. It always was —
the one field takes a recovery code or a TOTP code — but every string around it said only “Recovery
code”, so anyone with a backup authenticator app had no way to know. README.mdis now included in the plugin package.readme.txtrefers readers to it by name for the
verification-status detail and the fail-closed rollout warning, and it was being stripped out of the
distributed zip, so both references resolved to nothing.- The “External services” disclosure now lists every call the plugin makes, including the enrolment
read on the profile screen, the pairing-code poll and the unlink, and states explicitly what is never
sent.
0.2.2
- Fix: a refused second factor now answers HTTP 403 instead of 500. Every rejection in the login challenge —
a mistyped or already-used recovery code, a lapsed sign-in attempt, and every login attempt by a user in an
enforced role who has not enrolled yet — calledwp_die()with no arguments, andwp_die()defaults to- The page shown was always correct; only the status line said “Internal Server Error” for what is an
ordinary, expected refusal. It matters because managed hosts, firewalls and CDNs alarm and rate-limit on
5xx rates without reading the page, so a user fumbling their recovery codes could trip a false outage alert
or be throttled before reaching the login screen again. The one path that genuinely cannot complete — a
login fired from somewhere that cannot render the challenge screen — still answers 500, now explicitly.
- The page shown was always correct; only the status line said “Internal Server Error” for what is an
0.2.1
- Fix: the API key and webhook signing secret are stored exactly as entered. They were previously passed
throughsanitize_text_field(), which strips percent-encoded octets and tag-like sequences and collapses
whitespace — so a credential containing any of those was silently stored as a different string and every
later API call or webhook signature check failed with nothing on screen to explain it. Neither value is
ever rendered back into the settings form, so nothing was gained by sanitising them.
0.2.0
- Security: the API base URL is compiled into the plugin and is no longer an admin setting. The API key
is sent to whatever host that URL names, so an editable field let anyone who reached wp-admin redirect
the key to a server they control.U2AUTH_BASE_URLstill overrides it on local and staging installs,
and is ignored on production. - Signing in with a recovery code is now its own screen, reached from a “Use recovery code” link, rather
than a code field sitting under “approve on your phone”. - Step-up approval offers a recovery code. Previously an outage, a revoked enrolment or a rotated API key
sealed every gated capability behind a request that could never succeed, with no route back short of
shell access to the server. - Failures say what actually went wrong. A rejected API key and an unenrolled account were both reported
as “we could not reach U2 Secured”, which sent people looking for an outage that was not happening. - Fixed: updated CSS and JavaScript never reached browsers, because every asset URL carried a version
string that never changed. - The step-up screen’s text is now translatable; it was previously hardcoded English.
- Licence changed to GPLv2 or later.
0.1.0
- Initial release: tap-to-approve push challenge on login, TOTP and recovery-code fallback, role-based
enforcement with a grace period, step-up re-authentication for sensitive admin actions, XML-RPC /
Application Passwords handling, the optional signedapproval.resolvedwebhook receiver, a guided
setup panel with a connection test on the settings screen, and thewp u2auth disable/
wp u2auth status WP-CLI escape hatch.
