{"id":372663,"date":"2026-10-08T18:42:19","date_gmt":"2026-10-08T18:42:19","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/dollarmates-u2-secured-authenticator\/"},"modified":"2026-10-08T18:41:44","modified_gmt":"2026-10-08T18:41:44","slug":"dollarmates-u2-secured-authenticator","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/dollarmates-u2-secured-authenticator\/","author":23570690,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.3.1","stable_tag":"0.3.1","tested":"7.1.3","requires":"6.0","requires_php":"7.4","requires_plugins":null,"header_name":"DollarMates U2 Secured Authenticator","header_author":"","header_description":"Tap-to-approve two-factor authentication for WordPress.","assets_banners_color":"ea5f26","last_updated":"2026-10-08 18:41:44","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"","rating":0,"author_block_rating":0,"active_installs":0,"downloads":94,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.3.1":{"tag":"0.3.1","author":"dollarmates","date":"2026-10-08 18:41:44","revision":3735356}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3735355,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3735355,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3735355,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3735355,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.3.1"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[9211,602,285134,600,9217],"plugin_category":[38,54],"plugin_contributors":[285135],"plugin_business_model":[],"class_list":["post-372663","plugin","type-plugin","status-publish","hentry","plugin_tags-2fa","plugin_tags-login","plugin_tags-push-authentication","plugin_tags-security","plugin_tags-two-factor","plugin_category-authentication","plugin_category-security-and-spam-protection","plugin_contributors-dollarmates","plugin_committers-dollarmates"],"banners":{"banner":"https:\/\/ps.w.org\/dollarmates-u2-secured-authenticator\/assets\/banner-772x250.png?rev=3735355","banner_2x":"https:\/\/ps.w.org\/dollarmates-u2-secured-authenticator\/assets\/banner-1544x500.png?rev=3735355","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/dollarmates-u2-secured-authenticator\/assets\/icon-128x128.png?rev=3735355","icon_2x":"https:\/\/ps.w.org\/dollarmates-u2-secured-authenticator\/assets\/icon-256x256.png?rev=3735355","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>U2 Secured Authenticator adds push-based two-factor authentication to WordPress login. After a user's\npassword is verified, WordPress holds the session open (it does not complete it) and challenges for a\nsecond factor:<\/p>\n\n<ul>\n<li><strong>Tap-to-approve push<\/strong> \u2014 a number-match approval sent to the user's phone via the U2 Secured app.<\/li>\n<li><strong>TOTP fallback<\/strong> \u2014 a standard six-digit authenticator-app code, for when a push can't reach the\nphone. Verified <strong>on your own server<\/strong>: the RFC 6238 check runs in PHP against the secret this site\nalready holds (encrypted at rest), with the same \u00b11 window tolerance for clock drift, and no request\nleaves the site to do it. Requires the <code>sodium<\/code> PHP extension and an encryption key (see\nInstallation); the plugin says so plainly on the profile screen when it isn't available, rather than\nsilently dropping the option. Wrong codes are rate-limited per user (five inside fifteen minutes)\nso a six-digit code cannot simply be ground through.<\/li>\n<li><p><strong>Recovery codes<\/strong> \u2014 ten single-use codes issued when a user links their device, verified entirely\nlocally against WordPress's own database.<\/p>\n\n<p>Both fallbacks are what make the failure mode of an unreachable U2 API survivable: the push path is\ndead, but a linked user can still sign in with either, because neither makes an outbound call. They\nshare one field on the fallback screen, which accepts whichever the user has to hand.<\/p><\/li>\n<li><strong>Role-based enforcement with a grace period<\/strong> \u2014 require two-factor for chosen roles (e.g.\nAdministrator) without locking out every matching user the instant it's switched on. Each user gets a\nconfigurable number of days to link before they're actually blocked.<\/li>\n<li><strong>Step-up re-auth<\/strong> \u2014 a small set of sensitive admin capabilities (e.g. installing plugins) can\nrequire a fresh tap before they're usable, even mid-session.<\/li>\n<li><strong>Optional signed webhook<\/strong> \u2014 U2 Secured can POST the outcome of an approval straight to this site\nthe moment the user taps, so the login screen can answer from local state instead of asking again.\nIt is an optimisation and nothing more: every delivery must carry a valid signature to be believed,\nand login works identically on a site that never receives one, because the browser asks U2 Secured\nfor the outcome itself anyway. Sites that aren't reachable from the public internet should simply\nskip it.<\/li>\n<li><strong>XML-RPC \/ Application Passwords awareness<\/strong> \u2014 XML-RPC authenticates with the raw password and never\nreaches the <code>wp_login<\/code> hook this plugin relies on, so it's explicitly closed off. Application\npasswords are a legitimate WordPress feature, so instead of being silently broken they're surfaced to\nthe administrator as an admin notice.<\/li>\n<\/ul>\n\n<p>Every enrolment is opt-in until an administrator turns on role-based enforcement. Every error path \u2014\nan unreachable U2 API, a missing encryption key, an unrecognised device, <strong>a removed or rotated-out API\nkey<\/strong> \u2014 is designed to <strong>fail closed<\/strong>: the safe outcome is \"ask again\" or \"refuse,\" never \"let the\nrequest through.\" A user who requires a second factor on a site that can no longer perform one is\nrefused and told to ask an administrator; they are not quietly let in. See <strong>Verification status<\/strong> in\n    README.md for exactly what is and isn't proven by the automated tests, and please read it before\nenabling enforcement on a production site.<\/p>\n\n<h4>The escape hatch<\/h4>\n\n<p>If a user loses their phone <em>and<\/em> their recovery codes, the only way back in is an administrator\nrunning, over SSH:<\/p>\n\n<pre><code>wp u2auth disable &lt;user&gt;\n\n&lt;user&gt; accepts a WordPress user ID, login, or email address. This clears the plugin's local\n<\/code><\/pre>\n\n<p>two-factor state for that user (it does not touch anything on the U2 side, so it works even when the\nU2 API is unreachable) and immediately admits them again \u2014 they can re-link from their profile once\nback in. <code>wp u2auth status &lt;user&gt;<\/code> reports whether a user is currently linked and how many recovery\ncodes they have left, without changing anything.<\/p>\n\n<h3>External services<\/h3>\n\n<p>This plugin cannot work on its own: approving a login means asking a phone, and that request\ntravels through the U2 Secured Authenticator service at https:\/\/auth.u2secured.com. Installing\nthe plugin does not by itself send anything \u2014 nothing leaves your site until an administrator\nenters an API key and a user links a phone.<\/p>\n\n<p><strong>When the site contacts the service<\/strong><\/p>\n\n<ul>\n<li>When a user links their phone: to mint a one-time pairing code, then once every few seconds\nuntil the code is redeemed or expires, and once more to read the resulting link.<\/li>\n<li>When a linked user opens their own profile screen, to read whether their phone is still\nlinked and able to receive approvals.<\/li>\n<li>When a linked user signs in, to ask their phone to approve it, and then once every two\nseconds until they answer or the request expires.<\/li>\n<li>When a linked administrator performs a sensitive action (installing a plugin, editing a\nuser), for the same approval round trip.<\/li>\n<li>When a user unlinks their phone, to remove the link on the U2 side as well.<\/li>\n<li>When an administrator presses \"Test connection\" on the settings screen.<\/li>\n<\/ul>\n\n<p><strong>What is sent<\/strong><\/p>\n\n<ul>\n<li>An identifier for the user, in the form <code>&lt;random site id&gt;|&lt;numeric WordPress user ID&gt;<\/code>. The\nsite id is a random string generated once and stored in your database. <strong>Your users' email\naddresses, usernames and passwords are never sent<\/strong>, and neither is your site's URL.<\/li>\n<li>Your site's name (from Settings \u2192 General), so the person looking at their phone can see\nwhich site is asking.<\/li>\n<li>For a sensitive admin action, the name of that action \u2014 for example \"Install a plugin\".<\/li>\n<li>While a phone is being linked, the one-time pairing code the service itself just issued; while\nan approval is outstanding, the id of that approval. Neither is derived from anything about\nthe user.<\/li>\n<li>Your site's API key, to authenticate the request.<\/li>\n<\/ul>\n\n<p><strong>What is never sent<\/strong><\/p>\n\n<ul>\n<li><strong>Recovery codes and TOTP codes, and the TOTP secret itself.<\/strong> Both fallback factors are\nchecked inside PHP on your own server \u2014 recovery codes against hashes in your database, TOTP\nagainst the secret your site holds (RFC 6238, \u00b11 window) \u2014 so no request is made to check\neither, and the shared secret never leaves the site after enrolment. Earlier releases did send\nthe TOTP secret and the submitted code to the service to be checked; 0.3.0 stopped.<\/li>\n<li>Passwords, usernames, email addresses, and your site's URL.<\/li>\n<\/ul>\n\n<p>Service terms: https:\/\/auth.u2secured.com\/terms\nPrivacy policy: https:\/\/auth.u2secured.com\/privacy<\/p>\n\n<!--section=installation-->\n<ol>\n<li><p>Upload the plugin to <code>\/wp-content\/plugins\/dollarmates-u2-secured-authenticator<\/code> (or install the zip through\nPlugins \u2192 Add New), then activate it.<\/p><\/li>\n<li><p>Create an app in the U2 Secured developer portal and copy its <strong>server API key<\/strong>\n(<code>rka_...<\/code>). This key is what lets the plugin request push approvals, mint pairing codes and read\nenrolments. It is not used to check TOTP or recovery codes \u2014 both are verified on your own server.<\/p>\n\n<p><strong>Keep it in place once users have enrolled.<\/strong> Removing or rotating it out does not switch\ntwo-factor off; it makes the push factor impossible, and a user who requires a second factor is then\nrefused at login rather than let through. Use <code>wp u2auth disable &lt;user&gt;<\/code> (below) if you need to\nrelease someone.<\/p><\/li>\n<li><p>Add the API key to <code>wp-config.php<\/code>. A <code>wp-config.php<\/code> constant is preferred over the plugin's own\nsettings screen because it isn't copied into every database backup:<\/p>\n\n<p>define( 'U2AUTH_API_KEY', 'rka_...' );<\/p>\n\n<p>(The settings screen at Settings \u2192 U2 Secured still accepts it as a fallback for sites that can't\nedit <code>wp-config.php<\/code> \u2014 but the constant always wins, and the field is disabled on the settings screen\nonce it's set.)<\/p><\/li>\n<li><p>Add a second constant, <code>U2AUTH_ENCRYPTION_KEY<\/code>. <strong>This one gates a security control<\/strong>: it is the key\nused to encrypt the site-held TOTP secret at rest, so that a database dump alone doesn't hand over\nworking two-factor secrets alongside the accounts they protect. Without it, the TOTP fallback is\nunavailable (the plugin says so on the profile screen and in an admin notice; push and recovery\ncodes still work) rather than storing the secret unencrypted.<\/p>\n\n<p>define( 'U2AUTH_ENCRYPTION_KEY', '&lt;32 random bytes, base64&gt;' );<\/p>\n\n<p>Generate a value with:<\/p>\n\n<p>php -r 'echo base64_encode(random_bytes(32)), PHP_EOL;'<\/p>\n\n<p>Requires the <code>sodium<\/code> PHP extension, which ships enabled by default on PHP 7.2+.<\/p><\/li>\n<li><p>Visit <strong>Settings \u2192 U2 Secured<\/strong>. The <strong>Getting started<\/strong> panel there repeats these steps and\nreports which of them this site has actually completed, and <strong>Test connection<\/strong> checks the API key\nand base URL against U2 Secured with a single read-only request \u2014 so a wrong key is something you\nfind out by pressing a button, not by being locked out at your next login. The same screen is where\nyou optionally:<\/p>\n\n<ul>\n<li>pick which roles are required to use two-factor (leave every role unticked to keep linking opt-in),<\/li>\n<li>set the grace period, in days, a newly-required user gets before they're actually blocked.<\/li>\n<\/ul><\/li>\n<li><p><strong>Optional:<\/strong> set up the webhook. In the developer portal, point your application's webhook at the\nURL shown in step 4 of the <strong>Getting started<\/strong> panel. Take it from there rather than constructing\nit: the URL depends on your site address <em>and<\/em> your permalink settings, the panel shows the single\nform that is correct for your site as it is configured, and a stale URL in the portal fails\nsilently. On most sites it reads:<\/p>\n\n<p>https:\/\/example.com\/wp-json\/u2auth\/v1\/webhook<\/p>\n\n<p>and on a site using plain permalinks, <code>https:\/\/example.com\/?rest_route=\/u2auth\/v1\/webhook<\/code>. If you\nchange your permalink settings later, come back and copy the new URL across.<\/p>\n\n<p>Then give this site the signing secret the portal shows for that webhook, so it can tell a real\ndelivery from a forged one:<\/p>\n\n<p>define( 'U2AUTH_WEBHOOK_SECRET', '...' );<\/p>\n\n<p>(Again, the settings screen accepts it as a fallback. An unsigned, wrongly signed, or stale\ndelivery is rejected, and with no secret configured at all every delivery is rejected \u2014 \"not\nconfigured\" is never read as \"accept anything\".)<\/p>\n\n<p><strong>Skip this step entirely if the site isn't reachable from the public internet<\/strong> \u2014 a local install,\nan intranet site, anything behind NAT or a VPN. Nothing breaks: the login screen asks U2 Secured\nfor the approval outcome itself, and always did. All the webhook saves is an outbound call or two.<\/p><\/li>\n<li><p>Each user links their own device from their own profile screen (<strong>Users \u2192 Profile<\/strong>, or <strong>Your\nProfile<\/strong> in the admin toolbar) \u2014 an administrator cannot link a device on another user's behalf,\nsince linking inherently needs that user's own phone. Linking also issues ten single-use recovery\ncodes, shown once and never shown again, so tell people to save them somewhere they can reach\nwithout their phone.<\/p><\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"what%20happens%20if%20a%20user%20loses%20their%20phone%3F\"><h3>What happens if a user loses their phone?<\/h3><\/dt>\n<dd><p>They fall back to a recovery code (issued, ten at a time, when they first linked, and shown again any\ntime they choose to regenerate them from their profile) or a TOTP code from a backup authenticator app,\nif one was set up. If they've lost the phone <em>and<\/em> exhausted their recovery codes, an administrator\nruns <code>wp u2auth disable &lt;user&gt;<\/code> over SSH \u2014 see \"The escape hatch\" above. This is a local, offline\noperation: it works even if the U2 API itself is unreachable.<\/p><\/dd>\n<dt id=\"does%20turning%20on%20role-based%20enforcement%20lock%20everyone%20out%20immediately%3F\"><h3>Does turning on role-based enforcement lock everyone out immediately?<\/h3><\/dt>\n<dd><p>No. A user in a newly-enforced role gets the configured grace period (default seven days) to link\nbefore they're blocked \u2014 they're nagged on their profile screen, not refused at login, until that\nwindow closes.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20u2%20api%20is%20unreachable%3F\"><h3>What happens if the U2 API is unreachable?<\/h3><\/dt>\n<dd><p>The plugin fails closed, and there is no setting that changes that \u2014 opting out is not offered.<\/p>\n\n<p>In practice that is less dramatic than it sounds. An already-linked user cannot use the push challenge\nwhile U2 is unreachable, but can still sign in with a recovery code or a TOTP code: both are verified\nagainst this site's own data with no outbound call at all. The login is still gated, just via a\ndifferent factor. If the phone is gone <em>and<\/em> the codes are exhausted, <code>wp u2auth disable &lt;user&gt;<\/code> over\nSSH is the escape hatch, and it makes no network call either.<\/p>\n\n<p>What the plugin will not do is let a session through with no second factor because a network call\nfailed. See \"Fail-closed means a misconfiguration blocks logins\" under Verification status in\n    README.md for what that means for your own rollout.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20api%20key%20is%20removed%2C%20or%20rotated%20out%3F\"><h3>What happens if the API key is removed, or rotated out?<\/h3><\/dt>\n<dd><p>Users who require a second factor are refused, with a message telling them to ask an administrator to\ncheck the key. They are <strong>not<\/strong> let in.<\/p>\n\n<p>This is worth stating plainly because the alternative is the more tempting behaviour and it is a\ntwo-factor bypass: <code>wp_login<\/code> fires after WordPress has already set the auth cookie, so a plugin that\nmerely declines to challenge when it is unconfigured has thereby completed the login. Up to and\nincluding 0.2.2 this plugin did exactly that, and deleting the API key silently switched two-factor off\nfor every user who had it. Fixed in 0.3.0.<\/p>\n\n<p>A site with the plugin installed and nobody enrolled is unaffected: it behaves exactly as WordPress\ndoes, which is why the check asks \"does this user need a second factor\" before it asks \"can this site\nsend one\".<\/p><\/dd>\n<dt id=\"what%20stops%20someone%20guessing%20a%20totp%20code%3F\"><h3>What stops someone guessing a TOTP code?<\/h3><\/dt>\n<dd><p>Five wrong codes for one user inside fifteen minutes, and the TOTP factor refuses that user until the\nwindow passes. Recovery codes are checked before the limit applies, so a user who fumbled their\nauthenticator app still has their codes \u2014 the limit is a brake, never a lockout, and it clears itself.<\/p>\n\n<p>Each guess also costs a full password sign-in, because the challenge is single-use: a failed code\ninvalidates the attempt and the attacker has to authenticate the password again to get another.<\/p><\/dd>\n<dt id=\"do%20i%20need%20the%20webhook%3F\"><h3>Do I need the webhook?<\/h3><\/dt>\n<dd><p>No. It's an optimisation, never a requirement. When a user taps Approve, U2 Secured can POST that\noutcome to this site immediately; the login screen then answers from local state instead of asking U2\nSecured again. Without it the login screen simply asks \u2014 which is what it does anyway while it waits.\nEvery login flow, fallback and failure mode is identical either way.<\/p>\n\n<p>Set it up only if this site is reachable from the public internet. If it isn't, deliveries would never\narrive and nothing would tell you so, which is worse than not configuring it. The URL to paste into\nthe developer portal is printed on <strong>Settings \u2192 U2 Secured<\/strong>, under Getting started \u2014 take it from\nthere rather than typing it out, since it follows your site address and your permalink structure, and\nthat screen already resolves which of the two forms your site actually uses.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20someone%20forges%20a%20webhook%20delivery%3F\"><h3>What happens if someone forges a webhook delivery?<\/h3><\/dt>\n<dd><p>It's rejected. Every delivery carries an HMAC signature over the exact bytes sent, computed with the\nshared secret from <code>U2AUTH_WEBHOOK_SECRET<\/code> (or the settings screen), and is verified before a single\nfield of the payload is read. A wrong signature, a signature computed over different bytes, a stale\ntimestamp, or a malformed header are all refused and change nothing. If no secret is configured at\nall, every delivery is refused \u2014 \"not configured\" is never treated as \"accept anything\". A rejected\nwebhook can't strand a login either: the browser's own poll resolves the approval regardless.<\/p><\/dd>\n<dt id=\"does%20this%20affect%20xml-rpc%20or%20application%20passwords%20logins%3F\"><h3>Does this affect XML-RPC or Application Passwords logins?<\/h3><\/dt>\n<dd><p>XML-RPC authenticates with the raw password and never reaches the hook this plugin challenges on, so\nthe plugin closes it off outright rather than leaving a second, unguarded front door. Application\nPasswords are a legitimate WordPress feature with their own use cases, so instead of being blocked\nthey're surfaced as an admin notice \u2014 decide for your site whether to disable them separately.<\/p><\/dd>\n<dt id=\"can%20an%20administrator%20link%20or%20unlink%20a%20device%20for%20another%20user%3F\"><h3>Can an administrator link or unlink a device for another user?<\/h3><\/dt>\n<dd><p>No. Linking needs that user's own phone, so there is no \"link on behalf of\" flow. The only admin-side\naction is the <code>wp u2auth disable<\/code> \/ <code>wp u2auth status<\/code> escape hatch described above.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.3.1<\/h4>\n\n<ul>\n<li><strong>The TOTP attempt limit could be stretched by sending guesses in parallel.<\/strong> The five-per-15-minutes\ncounter was advanced by reading it, adding one, and writing the result back. Two submissions that\noverlapped both read the same number and both wrote the same number, so one of the two guesses cost\nnothing \u2014 making the limit a function of how many requests were sent at once rather than how many\nguesses were made. The counter is now incremented atomically: through the object cache's own\nincrement where a persistent cache is available, and otherwise by a single self-referential SQL\nUPDATE that the database serialises. Reported by the plugin review team.<\/li>\n<\/ul>\n\n<h4>0.3.0<\/h4>\n\n<ul>\n<li><strong>Security: a second factor could be skipped entirely on a site whose API key had been removed or\nrotated out.<\/strong> The login interception treated \"this user requires a second factor\" and \"this site can\nperform one\" as the same condition, and answered both by returning \u2014 but <code>wp_login<\/code> fires after\nWordPress has already set the auth cookie, so returning there completed the login. Any already-linked\nuser therefore signed in with a password alone once the key was gone, which is the opposite of what\nthis readme promised. Entitlement is now decided first and on its own: a user who needs a second\nfactor and cannot be given one is refused, with a message naming the fix. A site with nobody enrolled\nstill signs users in exactly as WordPress does.<\/li>\n<li><strong>TOTP codes are now verified on your own server, and the TOTP secret is no longer transmitted.<\/strong>\nVerification used to POST the shared secret <em>and<\/em> the submitted code to the U2 Secured API on every\nfallback login \u2014 while this readme said, in as many words, that TOTP codes are verified on your own\nserver and never transmitted. The RFC 6238 check (same \u00b11 drift window, same answer) now runs in PHP\nagainst the secret this site already holds, so nothing goes on the wire, and a U2 outage no longer\ntakes the TOTP factor down with push.<\/li>\n<li>Wrong TOTP codes are rate-limited locally: five per user inside fifteen minutes. The remote endpoint\napplied a brute-force lockout shared across its callers, and moving the check in-house would\notherwise have dropped that protection. Recovery codes are checked <em>before<\/em> the limit, so it can\nnever become a lockout, and a throttled user is told they are throttled rather than told their code\nwas wrong.<\/li>\n<li>The fallback screen now says that an authenticator-app code is accepted there too. It always was \u2014\nthe one field takes a recovery code or a TOTP code \u2014 but every string around it said only \"Recovery\ncode\", so anyone with a backup authenticator app had no way to know.<\/li>\n<li><code>README.md<\/code> is now included in the plugin package. <code>readme.txt<\/code> refers readers to it by name for the\nverification-status detail and the fail-closed rollout warning, and it was being stripped out of the\ndistributed zip, so both references resolved to nothing.<\/li>\n<li>The \"External services\" disclosure now lists every call the plugin makes, including the enrolment\nread on the profile screen, the pairing-code poll and the unlink, and states explicitly what is never\nsent.<\/li>\n<\/ul>\n\n<h4>0.2.2<\/h4>\n\n<ul>\n<li>Fix: a refused second factor now answers HTTP 403 instead of 500. Every rejection in the login challenge --\na mistyped or already-used recovery code, a lapsed sign-in attempt, and every login attempt by a user in an\nenforced role who has not enrolled yet -- called <code>wp_die()<\/code> with no arguments, and <code>wp_die()<\/code> defaults to\n\n<ol>\n<li>The page shown was always correct; only the status line said \"Internal Server Error\" for what is an\nordinary, expected refusal. It matters because managed hosts, firewalls and CDNs alarm and rate-limit on\n5xx rates without reading the page, so a user fumbling their recovery codes could trip a false outage alert\nor be throttled before reaching the login screen again. The one path that genuinely cannot complete -- a\nlogin fired from somewhere that cannot render the challenge screen -- still answers 500, now explicitly.<\/li>\n<\/ol><\/li>\n<\/ul>\n\n<h4>0.2.1<\/h4>\n\n<ul>\n<li>Fix: the API key and webhook signing secret are stored exactly as entered. They were previously passed\nthrough <code>sanitize_text_field()<\/code>, which strips percent-encoded octets and tag-like sequences and collapses\nwhitespace -- so a credential containing any of those was silently stored as a different string and every\nlater API call or webhook signature check failed with nothing on screen to explain it. Neither value is\never rendered back into the settings form, so nothing was gained by sanitising them.<\/li>\n<\/ul>\n\n<h4>0.2.0<\/h4>\n\n<ul>\n<li>Security: the API base URL is compiled into the plugin and is no longer an admin setting. The API key\nis sent to whatever host that URL names, so an editable field let anyone who reached wp-admin redirect\nthe key to a server they control. <code>U2AUTH_BASE_URL<\/code> still overrides it on local and staging installs,\nand is ignored on production.<\/li>\n<li>Signing in with a recovery code is now its own screen, reached from a \"Use recovery code\" link, rather\nthan a code field sitting under \"approve on your phone\".<\/li>\n<li>Step-up approval offers a recovery code. Previously an outage, a revoked enrolment or a rotated API key\nsealed every gated capability behind a request that could never succeed, with no route back short of\nshell access to the server.<\/li>\n<li>Failures say what actually went wrong. A rejected API key and an unenrolled account were both reported\nas \"we could not reach U2 Secured\", which sent people looking for an outage that was not happening.<\/li>\n<li>Fixed: updated CSS and JavaScript never reached browsers, because every asset URL carried a version\nstring that never changed.<\/li>\n<li>The step-up screen's text is now translatable; it was previously hardcoded English.<\/li>\n<li>Licence changed to GPLv2 or later.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>Initial release: tap-to-approve push challenge on login, TOTP and recovery-code fallback, role-based\nenforcement with a grace period, step-up re-authentication for sensitive admin actions, XML-RPC \/\nApplication Passwords handling, the optional signed <code>approval.resolved<\/code> webhook receiver, a guided\nsetup panel with a connection test on the settings screen, and the <code>wp u2auth disable<\/code> \/\n  wp u2auth status WP-CLI escape hatch.<\/li>\n<\/ul>","raw_excerpt":"Tap-to-approve two-factor authentication for WordPress logins, backed by the U2 Secured Authenticator app.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/372663","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=372663"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/dollarmates"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=372663"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=372663"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=372663"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=372663"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=372663"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=372663"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}