Skip to main content
Product Updates

AuthDock 2.0: SSO, Passkeys and Risk-Based 2FA for WordPress

WordPress authentication gets its enterprise half: single sign-on, ten social logins, passkeys, and two-factor that only interrupts when a sign-in looks unusual.

Arif Hossain
Arif Hossain
19 min read
ShareXLinkedInReddit
An abstract glass ring and geometric forms representing layered WordPress authentication

There is a moment every WordPress site owner eventually reaches. Two-factor authentication has been switched on for a few weeks, and someone — usually the person who publishes most — asks whether it can be turned off again. Not because they think security is unimportant, but because they have typed a six-digit code into the same laptop, in the same chair, at the same desk, every single morning, and nothing about that ritual has ever felt like it stopped anything.

They're not wrong. That's the actual failure mode of WordPress login security: not that the tools don't exist, but that the ones that exist are all-or-nothing, so people switch them off. And the day they do, the site goes back to a login form that will accept guesses forever and keep no record of who made them.

AuthDock 2.0 is our attempt to fix that shape. It's a free, GPL-licensed plugin, and this release is where it stopped being a collection of login features and became a complete authentication platform — single sign-on, risk-based two-factor, ten social providers, passkeys, network-wide policies, configuration as code, and an audit log that can prove it hasn't been edited. Version 2.1.6 is current as of 2 September 2026.

This post covers what actually changed, why each decision was made the way it was, and — because this matters more than the feature list — what the release deliberately does not do.

The short version

If you only read one paragraph: AuthDock 2.0 added enterprise single sign-on through any OpenID Connect provider, two-factor authentication that only interrupts when a sign-in looks unusual, six more social providers for ten in total, a passwordless-first mode built on passkeys and magic links, a login analytics screen, account lifecycle management, network-wide policies for multisite, configuration in wp-config.php or a JSON file in your repository, a hash-chained audit log with signed webhook streaming, and SMS as a last-resort factor. Then it rebuilt the admin around all of it: twenty-four menu items became seven, setup became a guided stepper, and every screen got a light and a dark theme.

Version 2.1 added Telegram as a notification channel. Version 2.1.6 was a full pre-release audit that added no features at all — more on that below, because it's the section most release posts would quietly skip.

Single sign-on, without an enterprise contract

This is the feature organisations actually buy an authentication plugin for, and until now WordPress made you choose between a paid connector and a weekend of OAuth debugging.

AuthDock 2.0 ships one configurable OpenID Connect provider, which turns out to cover nearly the entire market: Keycloak, Auth0, Okta, Authentik, Zitadel, Microsoft Entra ID, Google Workspace, Ping, FusionAuth, GitLab, Gitea, Nextcloud, and anything else that publishes a discovery document. You paste one URL. The endpoints, scopes, and signing keys configure themselves from that document. Several providers can run at once.

Two details are worth pulling out, because they're where these integrations usually go wrong.

Group-to-role mapping is checked against the signed token, not the browser. A directory group becomes a WordPress role — but the group membership is read from the provider's cryptographically signed token, verified on your own server against keys your provider publishes. Nothing a browser sent is trusted. And no new dependency was added to do it; the signature check uses what PHP already ships.

There are two guardrails, and both exist because of the same failure. The mapping will never demote your last remaining administrator, whatever the directory says. And when no rule matches an account, it leaves that account's roles alone rather than stripping them. A role-mapping bug that empties everyone's permissions at 9am on a Monday is a very bad morning, and it's the single most common way SSO integrations fail in practice.

Single sign-on needs your own identity provider — AuthDock connects to it, it doesn't host one. If you don't have a directory, the social providers and passkeys below are almost certainly what you want instead.

Two-factor that only interrupts when something looks wrong

This is the part that answers the person who wanted 2FA turned off.

Risk-based authentication scores each sign-in and asks for a second factor only when the sign-in looks unusual. Nine signals go into that score, and every one of them is computed on your own server:

  • a device the account has never used
  • an IP address it has never signed in from
  • a country it has never signed in from
  • two sign-ins further apart than anybody could physically travel between
  • an hour of the day the account never signs in at
  • recent failed attempts
  • whether the account can change the site (an administrator is worth more to an attacker than a subscriber)
  • whether the client looks like a real browser
  • whether the password is known to have appeared in a breach

No IP address is sent anywhere and no external service is called. There's no risk-scoring API behind this, no vendor holding a profile of your users' logins. That was a hard requirement, not an optimisation.

Three design decisions matter more than the signal list:

It watches before it acts. Switching the module on enforces nothing. It scores every sign-in and writes what it would have done into the audit log, so you can read a week of your own real traffic before deciding anything. Every decision records what it noticed, too — a challenge nobody can explain is a feature that gets switched off within a fortnight.

It only ever raises the bar. If your policy already requires a second factor for administrators, a low risk score does not excuse anybody. The engine can add friction; it can never remove it.

Refusing a sign-in outright is deliberately hard to reach. You have to move a Convenience-to-Security slider most of the way across before the engine is even allowed to refuse. The reasoning is asymmetric cost: a wrong challenge costs somebody thirty seconds, and a wrong refusal locks them out of their own site.

If you're coming to this from a standard setup, our WordPress login security guide still covers the baseline — lockouts, a moved login URL, enumeration prevention — and risk-based rules sit on top of it rather than replacing any of it.

Ten ways to sign in, and a password you can hide but never lose

AuthDock now supports ten social sign-in providers: Google, Facebook, GitHub, X, Apple, Microsoft, LinkedIn, Discord, Twitch, and Slack. Each has its own settings fields, brand mark, and working disconnect, and every card is generated from a provider registry rather than hand-written — which is also why a provider you register through the authdock_social_providers filter gets a full card of its own.

Two of the six additions carry most of the weight. Apple matters because an app offering any third-party sign-in is required to offer Apple too, and AuthDock treats Apple's hidden @privaterelay.appleid.com addresses as the real, verified addresses they are — refusing them, as some implementations do, would break Apple sign-in for everyone who chooses to hide their email, which is a lot of people. Microsoft unlocks essentially every business and education site.

Alongside that sits passwordless-first mode. One switch turns on passkeys and magic links together and folds the password field behind a "use a password instead" link. The security argument is straightforward: remove passwords and you remove the entire credential-stuffing surface. There's nothing to spray, nothing reusable from another site's breach, and nothing worth capturing on a convincing fake login page.

But note the wording — the password is hidden, never removed. It stays in the form, stays submittable, and is one click away. We built it that way after asking a specific question: what happens to a site that deletes the password field entirely and then loses access to its mail provider? Everyone without a passkey is locked out, and there's no route back. If the browser has already autofilled a password, the form is left exactly as it was.

An audit log that can prove it hasn't been edited

Every authentication event AuthDock records — sign-in, failure, logout, password change, registration, role change, session termination, lockout — lands in a purpose-built indexed table with the user ID, event type, IP address, user agent, JSON context, and timestamp.

What's new in 2.0 is that each entry is chained to the one before it with a keyed hash. Delete or edit a row and the chain breaks from that point onwards. A Verify Log Integrity button walks the chain and names the exact entry where the break starts.

Two things about it that we say on the plugin's own screen and will repeat here:

It reports; it never repairs. There's no button that quietly rebuilds a broken chain, because rebuilding it would destroy the only evidence that anything had happened.

It is tamper-evident, not tamper-proof. Anybody holding both your database and your WordPress salts can rebuild the chain and cover their tracks. What hash chaining actually defeats is the ordinary case — somebody with database access deleting the row that names them. That's a real and common scenario, and it's worth defending against; it just isn't the same claim as "tamper-proof," and we won't make the larger one.

From there, events can be streamed to any HTTPS webhook, signed with HMAC-SHA256, retried four times over about an hour, with the last fifty delivery attempts visible on screen. That's the shape Slack, Teams, PagerDuty, and every SIEM accept. Critically, a webhook is never in a sign-in's path — events go out after the response has been returned, so an endpoint that's slow, broken, or gone can never leave somebody waiting at your login form.

Sitting on top of the same data is a login analytics screen: sign-ins over time, which methods people used, why attempts were refused, which addresses failed most, which countries people came from, what hour of day people sign in, and how many accounts of each role have a second factor. The charts are drawn on your own server as plain SVG, from your own database. No chart library is loaded from a CDN and no data leaves the site.

For agencies and networks: policy you can commit to a repository

If you maintain more than a handful of WordPress sites, two 2.0 features change your workflow rather than your security posture.

Configuration as code. Define any setting with an AUTHDOCK_CONFIG array in wp-config.php, or commit an authdock.json to your theme or wp-content. Both take the same shape a settings export produces, so a value can be copied straight out of one.

// wp-config.php
define( 'AUTHDOCK_CONFIG', [
    'login_limiter' => [ 'enabled' => true, 'max_attempts' => 5 ],
    'two_factor'    => [ 'enabled' => true, 'enforce_roles' => [ 'administrator', 'editor' ] ],
    'audit_log'     => [ 'enabled' => true, 'retention_days' => 180 ],
] );

A managed setting shows as read-only on screen, with a badge saying where the value comes from, and the save path skips it — because a field you can edit and save and have silently ignored is worse than one that admits it's managed elsewhere. Nothing is written into the database, so removing the constant or the file puts the site back exactly as it was. And a broken authdock.json is ignored rather than fatal: a syntax error in a deployed file must never be the reason you can't administer your own site.

OAuth secrets already had their own constants, one per provider. A secret defined that way never reaches the database — not in your settings export, not in a backup, not in a screenshot of the settings screen.

Network-wide policies for multisite. A network administrator can require two-factor authentication, brute-force protection, audit logging, breached-password checks, or passkeys across every site in the network. Each control has three answers: the site decides, the network suggests, or the network requires. The default is the first, so an existing network changes in no way until somebody chooses otherwise.

Here's the constraint we're proudest of: a network policy can only make a site stricter. There is deliberately no way to switch a control off across every site at once. That would be a mechanism for disabling security on fifty sites with one click, possibly by accident, with no site administrator seeing it happen. Individual sites can be excused one at a time — a staging copy, a client sandbox — because the alternative is people switching the whole policy off to unblock one site.

Alerts where you actually are

AuthDock produces eight administrator alerts and six account-holder alerts: failed-login thresholds, IP lockouts, administrator logins, new registrations, role promotions to administrator, 2FA being disabled, sign-ins from a new address, password and email changes, plus a weekly digest.

Until 2.1, every one of those ended at wp_mail(). "Notification" and "email" were the same word. They're separate now: a handler describes what happened, and a dispatcher hands a copy to each channel the site has chosen. Email behaves exactly as it did, and a site that upgrades has no channel setting stored — an absent setting means email — so nothing changes for anyone until they change it.

Telegram ships as the second channel, and setup takes about four steps: message @BotFather, paste the token, press Connect Bot (the plugin verifies it with Telegram and fills in the bot's handle itself), post a message in the chat you want alerts in, press Detect Chat, then Send Test Message.

There's one implementation note worth stating because it's the opposite of what most integrations do: there is no webhook. A webhook would be a permanently open, unauthenticated public endpoint on a security plugin — and Telegram allows a bot exactly one, so the same token pasted into a staging site would silently take delivery away from production with no error anywhere. AuthDock reads the bot's own update queue instead, and only while somebody is actively waiting at the account screen for a link to complete. Nothing is exposed, and it works identically on localhost and behind a corporate firewall.

The rule behind all of it

Several 2.0 decisions share one principle, and it's the most distinctive thing about the release:

A change AuthDock makes on your behalf may only ever make a site more protected than it was.

You can see it everywhere once you know to look. The setup wizard's switches only go one way — a check that passes shows an "on" switch that can't be pressed, because turning a control off should be a decision you make on its own screen, having read what it does. Nothing the wizard does writes a false, empties a list, or loosens a policy. A network policy can only tighten. Risk scoring only raises the bar. The password is hidden, never removed. The log reports, and never repairs.

The reasoning is about threat models, not tidiness. A settings screen that can disable hardening in one click is a settings screen an attacker holding a valid admin session will use first.

What 2.1.6 fixed, and why we're telling you

Most release posts stop at the feature list. This one shouldn't, because version 2.1.6 was a full security, correctness, and stability audit of every module, and the recurring shape of what it found is worth knowing about whatever plugin you run.

The pattern was a control that reported itself as working while doing nothing at all.

A few examples. Location rules and bot protection refused sign-ins on WordPress's authenticate filter at priorities 12 and 15 — but core checks the password at priority 20, and core's authenticator discards an earlier refusal when a username and password are both present. So a denied country, a filled honeypot, and an unsolved challenge were each silently overwritten by a valid password. Account suspension had the same defect one priority worse. Location rules never applied at all on sites using a GeoLite2 database, because the reader resolved file pointers sixteen bytes early — and a rule with no data allows everyone by design, so the failure was silent and looked exactly like the rule working correctly. And social sign-in, magic links, and passkeys ignored the location rules, the IP lockout, and the risk engine entirely, because none of them calls wp_authenticate() — there's no password to check — so none of the authenticate filters ever ran.

All of those are fixed. The passwordless routes now ask one shared question, authdock_passwordless_login_allowed, which each module answers, so no control can be sidestepped by choosing a sign-in method that has no password.

We're publishing this for two reasons. First, if you ran AuthDock 1.x with country rules or bot protection enabled, you should know they weren't doing what the screen said. Second, the honest framing of 2.1.6 is that it's the release that made the 2.0 feature set trustworthy — and a security plugin that only ever publishes its wins is a security plugin you have no way to calibrate.

Upgrading from 1.x changes nothing about how people sign in. No setting is reset and nothing is switched on. What it changes is that several controls your settings screen said were protecting the site now actually are.

What a security plugin should cost you

Performance is the reason people uninstall security plugins, so 2.0 spent real effort here. Concrete numbers:

  • Five database queries removed from every page view. Five settings were stored in a way that excluded them from the single bulk fetch WordPress does at the start of every request, so each cost a separate query — on every page, including ones with no AuthDock content at all, and on every REST and admin-ajax call. Existing sites migrate automatically on their next page load.
  • Signing in with a passkey went from up to 500 queries to one. Finding which account holds a presented passkey used to ask the database about each candidate account individually.
  • The geolocation database is no longer read whole into memory. Answering "which country is this visitor in" touches a few hundred bytes; the reader was loading the entire file first — around 9 MB for the country database plus another 10 MB for the network one, on every sign-in that consulted them. It now holds at most a megabyte.
  • Front-end pages with no AuthDock content no longer load its translations.
  • The module registry was being rebuilt fourteen times per request. It isn't now.

There's also a caching fix that matters if you run LiteSpeed, Cloudflare, or any page cache. AuthDock now declares every authentication URL uncacheable as the plugin boots, before any cache has committed — and a Site Health check fetches your login URL the way a visitor does and reports what the edge says about it. That check exists because nothing inside WordPress can detect a cached login page from the inside: on the requests that matter, WordPress isn't running.

Getting started

If you're setting this up on a live site, the order that causes the fewest surprises:

  1. Install and activate. The setup stepper opens on first activation. Each step has its own URL and it resumes wherever you left off, so you can do it in two sittings. Seven steps cover every check the 0–100 security score grades you on.
  2. Set a recovery key before you move the login URL. The wizard offers this. Write it down. See locked out recovery for every break-glass route, including the wp-config.php constants that disable any control that can lock you out.
  3. Turn on the audit log first, and read it for a week. You cannot tune what you can't see, and the log is what makes every later decision evidence-based rather than a guess.
  4. Enable risk-based rules in watch mode. Same reasoning. Let it score a week of real traffic and record what it would have done before you enforce anything.
  5. Then choose your sign-in methods. Two-factor authentication per role, social providers, passkeys, or an OIDC connection — in whatever combination your users actually are.

The getting started guide walks through the first configuration in detail, and AuthDock sits alongside the rest of our WordPress plugins if you're already running SyncDock or GuestDock.

What it deliberately doesn't do

A few honest limits, because a features page that only lists strengths isn't much use for deciding:

  • No SMS provider ships with the plugin. SMS is offered as a second factor, but it's placed last and labelled as the weakest option — a SIM swap is a phone call to a carrier. Connecting Twilio, Vonage, MessageBird, AWS SNS, or a corporate gateway is about a dozen lines through the authdock_send_sms filter, and none of your messages pass through anything of ours.
  • AuthDock doesn't host an identity provider. SSO connects to yours.
  • It isn't a malware scanner or a firewall. It secures authentication and access. A vulnerable plugin is a different problem with a different tool.
  • It won't stop somebody who already has your database and your salts. See the tamper-evident note above.

And what it never does: there's no telemetry, no licence check, and no upsell. With every module switched off, the plugin makes no outbound request at all. Risk scoring, analytics, geolocation, and breached-password checking all run on your own server — breach checking uses k-anonymity, so no password and no full hash ever leaves the site. The only outbound requests AuthDock makes are ones you configured and can name.


AuthDock 2.1.6 is free on WordPress.org under the GPL-2.0-or-later licence, requires WordPress 6.0+ and PHP 7.4+, and is tested up to WordPress 7.1. Every feature described here is in that free version; there is no pro tier holding one of them back.

If you're weighing it against what you're running now, the AuthDock product page has the full feature breakdown — and if you'd rather ask a question first, get in touch.

Arif Hossain

Written by

Arif Hossain

WordPress & Plugin Engineer

WordPress & plugin engineer at Degird, building the plugins that harden and extend WordPress — GuestDock, AuthDock, and SyncDock.