Releases: bordoni/integration-workos
Release list
1.0.8
Added
- Change email from the WorkOS Users page (#33) — adds a "Change email" action to the WorkOS → Users admin page, alongside "Open in WorkOS" and "Send password reset". When a privileged user (
edit_users) changes another account's email it now commits immediately, skipping the emailed verification step that self-service still uses; the admin path also skips rate limiting, sends no notification, surfaces real conflicts, and logsemail_change.admin_changed. Removes the now-unusedchange_email_admin_bypass_verificationoption.
Fixed
- 500 in admin password-reset when
profileis empty (#33) — an emptyprofile(the common case) fataled the endpoint instead of resolving to the default login profile. Theprofilesanitize callback no longer hands the request object back as the sanitized value.
1.0.7
What's Changed
- Fix: Active environment no longer reverts on settings save. Saving WorkOS settings no longer resets
workos_active_environmenttostagingwhen the active environment field is absent from the form. The sanitizer now preserves the current active environment, including legacyworkos_global['active_environment']fallback state. (#34)
Full Changelog: 1.0.6...1.0.7
1.0.6
Added
- Change email (WorkOS-verified, conflict-guarded) (#22) — self-service and admin-triggered email-change flow. Self-service
[workos:change-email]shortcode, a "Change email" row action under the WorkOS column onwp-admin/users.php, and a "Change Email" panel on the user-edit screen. The new address is confirmed via hashed (HMAC-SHA256 +wp_salt('auth')) single-use, expiry-bounded tokens stored as_workos_pending_email_changeuser_meta and validated withhash_equals(WorkOS'semail_verificationendpoints can't verify a pending address); the old address simultaneously gets a one-click cancel link. Three new REST endpoints (initiate / confirm / cancel) are capability-gated, per-IP + per-user rate-limited, and enumeration-safe. Configurable conflict policy (blockdefault,allow_orphan,merge_request) prevents silently overwriting another local WP user. Commits to WorkOS first (update_user) then mirrors into WordPress, guarded by a 60-second in-progress transient that short-circuits the webhook fan-back. Adds 8 filters, 5 actions, 7 activity-log event types, and 40 WPUnit tests. Seedocs/change-email.md. - Per-form magic-code registration toggles (CONS-350) (#25) — two independent per-environment checkboxes on the WorkOS settings page gate whether an unknown email signing in with a magic code provisions a new account. Email Code Registration controls the default sign-in form (
/login/); Legacy Email Code Registration controls the legacy form (/login/legacy/, slug filterable viaworkos_legacy_profile_slug). When a form's toggle is off,POST /auth/magic/sendskips the WorkOS call for unknown addresses and still returns200 ok: true, andPOST /auth/magic/verifyearly-returns a generic400 workos_authkit_invalid_codeinstead of proceeding toLoginCompleter/UserSync— closing the account-enumeration leak wheresendpreviously returned404 workos_authkit_no_account. Both options (allow_magic_code_registration,allow_legacy_magic_code_registration) default totrue, preserving historical behavior.
Fixed
- Username generation could exhaust memory and block provisioning (CONS-513) (#27) — when a popular email local part (e.g.
info@) already had a deepinfo_1,info_2, … collision chain,generate_username()probed it oneusername_exists()call at a time, hydrating aWP_Userinto the object cache per probe until the request OOM'd — so those users could log in via WorkOS but never got a WordPress account. Collisions are now resolved with a suffix derived fromsha256( email )(e.g.info_48f25), which is unique by construction: at most 2 lookups regardless of chain depth, and the same email always derives the same username, so crash retries and replayed webhooks converge instead of minting new names. Existing accounts keep their current usernames. - WorkOS → Users settings would not save — the "Sync Roles to WorkOS" button rendered its own
<form>inside the Settings API<form action="options.php">. Browsers don't allow nested forms: the inner</form>closed the outer settings form early, leaving the "Save Settings" submit button outside the form, so saving did nothing. The sync form is now deferred toadmin_footer(body level, like the Create Organization modal) and its button is wired back via the HTML5form="workos-role-sync-form"attribute, so the settings form stays intact and saves normally.
Full Changelog: 1.0.5...1.0.6
1.0.5
Added
- WorkOS → Users admin page (CONS-273) — new submenu under WorkOS that mounts a paginated, searchable React list of WorkOS users for the active environment. Each row exposes an "Open in WorkOS" deep-link straight to the user's Dashboard page (
https://dashboard.workos.com/{env}/users/{id}/details), where the per-user "Re-enable email" action lives. Gated bymanage_options. Backed byGET /wp-json/workos/v1/admin/users, which proxiesApi\Client::list_users()with sanitizedlimit(1..100), cursor (after/before),emailsubstring, andorganization_id, and enriches each user record with a server-computeddashboard_url. List-only by design: WorkOS does not expose a public REST endpoint for the "Re-enable email" action as of this release. - Admin-triggered WorkOS password reset (#21, CONS-287) — privileged WP users can now send a WorkOS password-reset email on behalf of any linked user via
POST /wp-json/workos/v1/admin/users/{id}/password-reset. Gated byedit_user($id)(so the same route covers self-service from the shortcode), rate-limited per-IP and per-target, and writes apassword_reset.admin_sentevent to the activity log. Triggered from three surfaces: aSend password resetrow action under the WorkOS column onwp-admin/users.php(clustered with "View in WorkOS" and "Re-sync" via the newworkos_user_list_column_actionsfilter), aPassword Resetpanel on the user-edit screen, and a[workos:password-reset]shortcode that toggles between admin-of-other (user="…") and self-service modes. redirect_urlparameter on admin + public reset endpoints — the value is validated same-host againsthome_url(), baked into the URL handed to WorkOS for the email, and passed back to the React shell so the user lands on the chosen page after a successful reset. Fixes the long-standing CONS-287 ("reset password redirects users to Kadence Central") where the post-reset URL was unconfigurable.- In-site React reset page — reset emails now point at
/workos/login/{profile}?token=…&redirect_to=…instead ofwp-login.php. The oldwp-login.php?workos_action=reset-passwordURL still resolves cleanly viaLoginTakeover, so reset emails already in users' inboxes continue to work. - Password strength + confirmation on reset — the
ResetConfirmstep now requires the user to enter the new password twice and scores it via WordPress'swp.passwordStrength.meter(zxcvbn) in real time. The submit button stays disabled until the two fields match and the score reachesStrong(zxcvbn ≥ 3). Site name and common words are passed as the zxcvbn disallowed list. - Auto-login after password reset — new per-profile toggle
auto_login_after_reset(default: on). When enabled, a successful reset authenticates the user via the sharedLoginCompleter(so MFA / organization selection / entitlement gates still apply) and lands them on the validated post-reset redirect. Toggle off restores the prior "Continue to sign in" card. - WP password mirror on reset — when a WorkOS reset succeeds and the WorkOS user is linked to a WP user (via
_workos_user_idmeta), the plugin also runswp_set_password()on the linked WP user with the new plaintext, keeping the WordPress password-fallback paths (?fallback=1,wp_authenticate, REST app passwords) in sync. Best-effort: unlinked users no-op cleanly; a write failure never fails the reset response. - Skeleton placeholders on every AuthKit surface — wp-login.php takeover,
/workos/login/{profile}, and[workos:login]shortcode now paint a card-shaped skeleton with shimmering placeholder rows the moment the page lands, instead of a blank gap while the React bundle downloads andclient.bootstrap()resolves. Heights match the hydrated form exactly so the swap is a flicker, not a layout jump. Shimmer animates onlybackground-position(no transforms, no opacity) and is disabled underprefers-reduced-motion.
Full Changelog: 1.0.4...1.0.5
1.0.4
Fixed
- Logged-out screen leaks classic wp-login (#18) — both
LoginTakeover::should_takeover()andLogin::maybe_redirect_to_authkit()short-circuited on?loggedout=true, so the native "you have been logged out" screen rendered with the wp-login username/password field. Legacy customers misread that as a still-working classic sign-in and kept trying to use it even though their credentials live in WorkOS. The bypasses are removed; with acustom_path-equipped default profile,/wp/wp-login.php?loggedout=truenow 302s to/login/?loggedout=truevia the existingredirect_to_custom_pathforwarder. The?fallback=1,?workos=0, andaction=logout|lostpassword|rp|...bypasses are unchanged. - Password reset URLs HTML-encoded in WorkOS emails (#17) —
wp_login_url()runs through thelogin_url/home_urlfilters; a host-side filter piped it throughesc_url(), encoding&→&. WorkOS emailed the URL verbatim, so reset links arrived as?workos_action=…&profile=…&token=…and broke. The plugin now decodes HTML entities (named and numeric) inbuild_password_reset_url()before POSTing to WorkOS. A regression wpunit test fails without the fix. - Infinite login redirect loops (#15) — auth redirects now send no-cache / no-store headers so a cached redirect response cannot trap the browser in a redirect cycle when the user navigates back to a previously visited URL.
- Unchecking auth methods or MFA factors did not persist (#14) —
update_profileinAdmin/LoginProfiles/RestApi.phpranarray_replace_recursive(\$existing, \$params)so partial PUTs only needed to send touched fields. For numerically-indexed lists that function merges by key and never removes entries that exist in the base but are absent from the override, so unchecking `oauth_google` on a profile with `['password', 'magic_code', 'oauth_google']` left `oauth_google` at index 2 of the merged result. After the merge, `methods` and `mfa.factors` are now explicitly overwritten from the incoming payload when the client sent them — `array_key_exists()` is used (not `isset()`) so an empty array (uncheck-everything save) is honored. Scalars and associative branches like `signup`, `branding`, and the `mfa` envelope itself were unaffected.
Tests
- Adds two cases in `AuthKitLoginProfilesRestApiTest`: `test_update_methods_payload_replaces_existing` and `test_update_mfa_factors_payload_replaces_existing` — start with multiple values, PUT with fewer, assert dropped values are gone in both the response and a fresh repository read; the second also asserts the sibling `mfa.enforce` survives the partial merge.
- Adds a wpunit regression covering HTML-entity decoding in `build_password_reset_url()`.
Full Changelog: 1.0.3...1.0.4
1.0.3
Fixed
-
organization_selection_requiredrecovery. AuthKit login flows (magic code, password, MFA verify, invitation accept) and the legacy/workos/callbackOAuth handler now recover transparently when WorkOS returnsorganization_selection_required.LoginCompleter::complete()accepts theWP_Errordirectly, looks up the Profile's pinnedorganization_id(withConfig::get_organization_id()as fallback), and re-authenticates via theurn:workos:oauth:grant-type:organization-selectiongrant. Users no longer see "The user must choose an organization to finish their authentication." when their Login Profile has an org pinned. -
Auto-enroll for legacy users. When the pinned org isn't in WorkOS's candidate list AND a matching WP user already exists locally, the plugin self-heals by creating the WorkOS organization membership and retrying the org-selection grant. The flow strictly requires the authenticated
user_idfrom the WorkOS error body — if it's absent, the request is rejected withworkos_authkit_pinned_org_mismatchrather than guessing via alist_users(email=…)lookup that can collide on shared addresses. Membership creation andentity_already_existsshort-circuits log viaworkos_log()so prod incidents have a breadcrumb. -
Unified OAuth callback.
/workos/callback(Login::handle_callback) now routes theauthenticate_with_coderesult throughLoginCompleter::complete(), so it shares the same recovery + MFA gating + post-login bookkeeping as the AuthKit REST endpoints. Legacy AuthKit-redirect callbacks (no profile slug instate) pass$honor_profile_redirect = falseso the state-suppliedredirect_tostill wins over the default profile'spost_login_redirect— pre-refactor contract preserved. -
Plugin::$versionsynced. The duplicateprivate string $versiononWorkOS\Pluginwas still reporting1.0.1. Updated to1.0.3so any runtime consumer (admin badge, debug output) reports the real shipped version.
Tests
- New
AuthKitLoginCompleterOrgSelectionTest(9 wpunit cases) covering each recovery branch. - New
ApiClientAuthKitTest::test_authenticate_with_organization_selection_uses_grant.
Internal
- New
Client::authenticate_with_organization_selection( $pending_auth_token, $organization_id ).
Full changelog: 1.0.2...1.0.3
1.0.2
Added
- WordPress password fallback — when WorkOS rejects a password, the
auth endpoint can now retry againstwp_authenticate()to cover users
whose passwords were never synced to WorkOS (e.g. accounts that existed
before the plugin was installed). On success the WP user is linked /
synced to WorkOS and, by default, the password is written through to
WorkOS so future logins authenticate directly. A new
Require Email Confirmation on Fallback setting (option
wp_password_fallback_email_confirmation) switches the post-fallback
step to a magic-code email instead of syncing the plaintext password.
Gated by the existingallow_password_fallbacksetting (default on). - wp-config.php constant seeder — defining
WORKOS_*(or
env-scopedWORKOS_{PRODUCTION|STAGING}_*) constants inwp-config.php
now seeds the corresponding settings into the database on boot, so the
admin UI reflects them and runtime reads stay on the existing options
layer. Covers string credentials (WORKOS_CLIENT_ID,WORKOS_API_KEY,
…), the new boolean toggles (WORKOS_ALLOW_PASSWORD_FALLBACK,
WORKOS_WP_PASSWORD_FALLBACK_EMAIL_CONFIRMATION), and array values
(WORKOS_REDIRECT_URLS). Skipped via a stored hash check
(workos_constants_hashoption) when nothing has changed — steady-state
cost is one autoloadedget_option()call per request.
Fixed
- REST nonce header — auth endpoints under
/wp-json/workos/v1/auth/*
now read the nonce fromX-WorkOS-Nonceinstead ofX-WP-Nonce. The
prior name collided with the header WP core and other plugins consume,
which could cause the wrong nonce to be validated when multiple
scripts attached to the same request. The bundled React shell sends
the new header automatically; external clients calling these endpoints
directly need to update their header name.
Tests
- Adds
ConfigSyncConstantsTest(11 wpunit cases) covering the new
constant seeder: string / bool / array maps, env-specific override
precedence, empty-string skip, the hash short-circuit, and the
trailing in-memory cache reset. The fixture definesWORKOS_*
constants insetUp(); because PHP cannot undefine a constant and
Codeception 5 ignores PHPUnit's process-isolation directives, the
class is tagged@group constantsand CI now runs it as a dedicated
codecept runinvocation. Defaultslic run wpunitskips the group.
1.0.1
Added
- Organization tab — manual Refresh button next to the organization
dropdown that re-fetches the WorkOS organization list on demand via the
admin REST endpoint (GET /workos/v1/admin/profiles/organizations?refresh=1),
bypassing the 5-minute transient cache. The dropdown is visually blocked
with a spinner while the request is in flight; the previously selected
organization is preserved across the refresh when it still exists. ?refreshquery parameter on
GET /wp-json/workos/v1/admin/profiles/organizations— drops the shared
workos_organizations_cache_{env}transient before fetching from WorkOS.
Fixed
- Organization tab — fix "Save Settings" being blocked by an invalid hidden
form control (org_name). The Create Organization Thickbox modal is now
rendered atadmin_footerso its inner<form>no longer nests inside
the outer settings form, preventing HTML5 constraint validation on the
hidden required field from aborting the parent form's submission. - Active environment —
workos_active_environmentis now the single source
of truth for the active environment. The admin Settings UI wrote the
user's selection to a standalone option while the runtime auth flow read
fromworkos_global['active_environment'], so picking "Production"
saved successfully but the runtime kept loading staging credentials and
redirecting users to the staging AuthKit.Config::get_active_environment()
/set_active_environment()now read and write the standalone option,
with a one-time migration (db_version 2 → 3) that copies any legacy
value out ofworkos_globaland unsets it.
Version: 1.0.0
First stable release of Integration with WorkOS — enterprise identity for WordPress, powered by WorkOS.
Highlights
Custom AuthKit — WordPress-hosted login experience
A React login shell mounted inside WordPress. All WorkOS API calls stay server-side; the browser only ever talks to /wp-json/workos/v1/auth/*.
- Login Profiles — admin-defined presets at WorkOS → Login Profiles (sign-in methods, pinned organization, signup / invitation / password-reset flows, MFA policy, branding, custom URL paths).
- wp-login.php takeover —
action=loginrenders the React shell; everything else passes through to WordPress defaults. - Sign-in methods — email + password, magic code, social OAuth (Google, Microsoft, GitHub, Apple), passkey.
- MFA — TOTP, SMS, WebAuthn / passkey. In-app enrollment, challenge, and verify. Per-profile
mfa.enforceand factor allowlist. - Self-serve flows — sign-up with email verification, invitation acceptance, password reset — all in-app.
- WorkOS Radar integration — browser action token forwarded on every server-side auth call.
- Profile routing rules — ordered
redirect_toglob /referrer_host/user_rolematchers pick the right profile per request. - SlotFill extensibility — 10 named React slots for third-party plugins to inject UI without forking the shell.
Base platform
- SSO via WorkOS AuthKit (redirect or headless), Directory Sync (SCIM) with provisioning + deprovisioning, Role Mapping WorkOS → WP, Organization Management with multisite support, Entitlement Gate.
- Webhook processing — real-time user, organization, and directory events at
/wp-json/workos/v1/webhook. - REST API authentication — Bearer token auth for headless / decoupled WordPress.
- Activity & Audit Logging — local table viewer + WorkOS Audit Logs forwarding.
- Role-based login & logout redirects, password-reset & registration redirects, login bypass via
?fallback=1. - Diagnostics page, Onboarding wizard, Admin Bar badge, WP-CLI commands for scripting and bulk operations.
Security & internationalization
- Webhook signature verification, atomic invitation token consumption, MFA enforcement at challenge time, per-IP and per-email rate limits on every auth mutation, profile-scoped WP nonces.
- Browser i18n via
@wordpress/i18nwith fullintegration-workostext-domain support.
Screenshots
Requirements
- WordPress 6.2+
- PHP 7.4+
- A WorkOS account
Installation
Download the attached .zip, then in WordPress admin go to Plugins → Add New → Upload Plugin. Activate, then visit Settings → WorkOS to add your API credentials. The Onboarding wizard at WorkOS → Onboarding walks through first-time setup.
Links
- Full changelog: CHANGELOG.md
- Documentation: README.md
- Report a bug: GitHub Issues



