Enterprise identity management for WordPress powered by WorkOS. SSO, directory sync, MFA, and user management.
Requires PHP: 7.4+ Requires WordPress: 5.9+ License: GPL-2.0-or-later
- React login shell on wp-login.php,
[workos:login]shortcode, and a dedicated/workos/login/{profile}route — all driven by the same TypeScript bundle - Login Profiles — admin-defined presets (enabled methods, pinned organization, signup/invite/reset toggles, MFA policy, branding) managed through a React admin editor at WorkOS → Login Profiles
- Sign-in methods: email + password, magic code, social OAuth (Google, Microsoft, GitHub, Apple), passkey
- In-app flows: self-serve sign-up with email verification, invitation acceptance, password reset
- MFA — TOTP, SMS, WebAuthn/passkey with full enrollment + challenge UI; profile-level
mfa.enforce(never/if_required/always) and factor allowlist - Profile routing rules — ordered
redirect_toglob /referrer_host/user_rolematchers pick the right profile per request - WorkOS Radar anti-fraud integration — browser SDK supplies an action token the plugin forwards on every server-side auth call
- No WorkOS PHP SDK — every WorkOS call is
wp_remote_requestfrom PHP, so the API key never leaves the server
- Single Sign-On (SSO) via WorkOS AuthKit — redirect or headless login modes (legacy path, selectable per-profile)
- Directory Sync (SCIM) — automatic user provisioning and deprovisioning from your identity provider
- Role Mapping — map WorkOS organization roles to WordPress roles
- Organization Management — local caching of WorkOS organizations with multisite support
- Entitlement Gate — require organization membership to log in
- Webhook Processing — real-time sync of user, organization, and directory events
- REST API Authentication — Bearer token auth for headless/decoupled WordPress
- Legacy Login Button — Gutenberg block and classic widget (AuthKit-redirect flow)
- Login Bypass — access the native WordPress login form via
?fallback=1when WorkOS is unavailable - Activity Logging — local database table with admin viewer for tracking authentication and sync events
- Audit Logging — forward WordPress events (login, logout, post changes, user changes) to WorkOS Audit Logs
- Role-Based Login Redirects — send users to different URLs after login based on their WordPress role
- Role-Based Logout Redirects — send users to different URLs after logout based on their WordPress role
- Password Reset Integration — redirect password reset to WorkOS or fall back to WordPress
- Registration Redirect — redirect registration to WorkOS AuthKit
- Admin Bar Badge — shows the active WorkOS environment (production/staging) in the admin bar
- Changelog Page — in-admin changelog viewer rendered from CHANGELOG.md
- Diagnostics Page — system health checks and configuration status
- Onboarding Wizard — guided setup for initial plugin configuration and user sync
- WP-CLI Commands — full CLI access for scripting, bulk operations, and diagnostics
- Download the latest
.zipfrom the Releases page. - In WordPress admin, go to Plugins > Add New > Upload Plugin and upload the ZIP file.
- Activate the plugin.
- Navigate to Settings > WorkOS to configure.
- Clone the repository into
wp-content/plugins/integration-workos/. - Run
composer installto install PHP dependencies. - Run
bun install && bun run buildto install JS dependencies and build assets. - Activate the plugin in WordPress admin.
- Navigate to Settings > WorkOS to configure.
Go to Settings > WorkOS in the WordPress admin. The settings page has three tabs:
| Tab | Contents |
|---|---|
| Settings | API Key, Client ID, Environment ID, webhook secret, login mode, password fallback, audit logging toggle |
| Organization | Select or create a WorkOS organization |
| Users | Deprovision action, content reassignment, role mapping table |
The plugin supports two environments — Production and Staging — with separate credentials for each. An admin bar badge shows which environment is active.
All credentials can be set via constants in wp-config.php, which take precedence over database values:
// Generic (used for any environment)
define( 'WORKOS_API_KEY', 'sk_live_...' );
define( 'WORKOS_CLIENT_ID', 'client_...' );
define( 'WORKOS_WEBHOOK_SECRET', 'whsec_...' );
define( 'WORKOS_ORGANIZATION_ID', 'org_...' );
define( 'WORKOS_ENVIRONMENT_ID', 'environment_...' );
define( 'WORKOS_ENVIRONMENT', 'production' ); // Lock active environment
// Per-environment (takes priority over generic)
define( 'WORKOS_PRODUCTION_API_KEY', 'sk_live_...' );
define( 'WORKOS_STAGING_API_KEY', 'sk_test_...' );The plugin ships three login modes. Each Login Profile picks its mode:
custom uses the React shell, authkit_redirect uses the legacy redirect
path. login_mode=headless (global) remains available for custom forms.
wp-login.php?action=login is intercepted on login_init and the React shell
renders in its place. All other actions — logout, register,
lostpassword, resetpass, confirmaction, postpass, ?fallback=1,
?workos=0 — pass through to core WP so WooCommerce, WP-CLI password
resets, and email confirmation links keep working.
The shell also mounts on [workos:login profile="slug"] and the
/workos/login/{profile} rewrite. Every mount reads configuration from
data-* attributes emitted by Auth\AuthKit\Renderer and talks to
/wp-json/workos/v1/auth/* for everything — no WorkOS calls are proxied
through the browser.
Set a Login Profile's mode to authkit_redirect and wp-login.php sends
users to WorkOS's hosted AuthKit. WorkOS returns to /workos/callback where
the plugin exchanges the authorization code. Use this mode for SSO
(SAML/OIDC) or any profile that needs WorkOS-hosted UX.
The plugin intercepts WordPress's authenticate filter and validates
credentials directly against the WorkOS API using email and password. This
mode is useful for custom login forms you render yourself.
When enabled, WordPress native password authentication remains available alongside WorkOS. Password reset and registration forms fall back to WordPress defaults.
If WorkOS is down or misconfigured, users can access the native WordPress
login form by appending ?fallback=1 to the login URL. This bypasses the
WorkOS redirect — and the React shell takeover — entirely.
A Login Profile is a stored configuration unit that governs a single login
entry point. Manage profiles under WorkOS → Login Profiles (React
editor backed by /wp-json/workos/v1/admin/profiles).
Each profile stores:
| Field | Purpose |
|---|---|
slug |
URL-friendly id; reserved default drives wp-login.php takeover |
custom_path |
Optional arbitrary URL path (e.g. members, team/login, login) that mounts the same React shell — see "Custom paths" below. Empty means only /workos/login/{slug} is registered. Available on every profile, including the reserved default (when set, /wp-login.php?action=login 302s to it) |
title |
Admin-facing label |
methods[] |
Enabled sign-in methods (any subset of password, magic_code, oauth_google, oauth_microsoft, oauth_github, oauth_apple, passkey) |
organization_id |
Server-side pinned WorkOS org; passed on auth calls |
signup |
{enabled, require_invite} — toggles self-serve signup |
invite_flow |
Allow invitation acceptance |
password_reset_flow |
Allow in-app password reset |
mfa |
`{enforce: never |
branding |
{logo_mode, logo_attachment_id, primary_color, heading, subheading} — logo_mode is default / custom / none (see "Logo modes" in docs/extending-the-login-ui.md) |
post_login_redirect |
URL the React shell navigates to on success (beats redirect_to) |
forward_query_args |
When true, appends safe inbound query args (utm_*, ref, custom params — never redirect_to, _wpnonce, loggedout, wp_lang, workos_*, etc.) to the post-login destination |
mode |
custom (React) or authkit_redirect (legacy) |
The branding.logo field defaults to the WordPress Site Icon when no
per-profile logo is set. See
docs/extending-the-login-ui.md for the
full developer guide on injecting React elements (SlotFill), enqueuing
per-profile CSS/JS, and the available PHP filters.
Any profile (including the reserved default) can claim an arbitrary
URL path on top of the canonical /workos/login/{slug} rewrite. Set
the Custom path field in the editor to e.g. members and
https://yoursite.com/members/ mounts the same React shell. The
shortcode [workos:login profile="members"] keeps working too.
- Both URLs are always live — adding a custom path never disables the
canonical
/workos/login/{slug}rewrite. - When the default profile owns a non-empty
custom_path,/wp-login.php?action=login302s to it with every inbound$_GETpreserved (soredirect_to,interim-login,reauth,instance,wp_lang, etc. survive the bounce).?loggedout,?fallback=1,LoginBypass, and non-loginactions short-circuit the redirect. - Reserved segments (
wp-admin,wp-includes,wp-content,wp-json,workos,feed,comments,trackback) are rejected at save time so you can't shadow core URLs.login,admin, andsigninare intentionally allowed so the default profile can bounce/wp-login.phpto a clean URL. - Saving a profile triggers exactly one soft
flush_rewrite_rules( false )on the next request when the custom-path set actually changes (signature stored in theworkos_custom_paths_signatureoption).
A visitor that hits any AuthKit surface (wp-login.php takeover,
/workos/login/{slug}, a custom path) while logged in is 302'd
straight to their post-login destination. The precedence is centralized
in Auth\AuthKit\LoginRedirector::for_visitor( Profile $profile ):
profile post_login_redirect → validated redirect_to → admin_url().
The [workos:login] shortcode can't redirect from inside the_content
(headers are already sent), so it renders an inline "You're already
signed in as {name}. [Continue]" notice that links to the same URL.
Rules stored under the workos_profile_routing_rules option pick the right
profile when no slug is explicit. Each rule is
{ profile: slug, matcher: { type, value } } where type is one of
redirect_to (glob), referrer_host (exact host), or user_role (role slug).
Rules evaluate top-down; first match wins; the default profile is the
fallback.
Profile-level MFA enforcement is applied by Auth\AuthKit\LoginCompleter:
enforce: never— single-step login is always acceptedenforce: if_required(default) — MFA step surfaces when WorkOS returns a pending factor; otherwise single-step is acceptedenforce: always— single-step success is rejected withworkos_authkit_mfa_required; the user must enroll a factor first
Factor types WorkOS returns in a pending-factor response are checked against
the profile's mfa.factors allowlist; disallowed types are rejected.
Set workos_radar_site_key (plugin option) or define
WORKOS_RADAR_SITE_KEY to enable Radar. The React shell loads
https://radar.workos.com/v1/radar.js, fetches an action token, and the
plugin forwards it as X-WorkOS-Radar-Action-Token on every
user-management auth call, so WorkOS can score the attempt server-side.
Configure your WorkOS dashboard to send webhooks to:
https://yoursite.com/wp-json/workos/v1/webhook
The plugin processes these event types:
| Category | Events |
|---|---|
| Users | user.created, user.updated, user.deleted |
| Directory Sync | dsync.user.created, dsync.user.updated, dsync.user.deleted, dsync.group.user_added, dsync.group.user_removed |
| Organizations | organization.created, organization.updated |
| Memberships | organization_membership.created, organization_membership.updated, organization_membership.deleted |
| Connections | connection.activated, connection.deactivated, connection.deleted |
| Auth | authentication.email_verification_succeeded |
All events are verified against the webhook signing secret before processing.
The plugin adds Bearer token authentication to the WordPress REST API. Send the WorkOS access token in the Authorization header:
Authorization: Bearer <workos_access_token>
The token is verified using WorkOS JWKS and mapped to a WordPress user via their linked WorkOS ID.
The plugin exposes a stable, read-only API for checking WorkOS state on
a WP user so integrations don't need to know what meta keys the plugin
stores. Everything lives in WorkOS\User (instance-free static class)
with matching global function shortcuts.
| Method | Returns | Purpose |
|---|---|---|
User::is_sso( $user_id = 0 ) |
bool |
User is linked to a WorkOS identity (persistent) |
User::has_active_session( $user_id = 0 ) |
bool |
User currently has a stored WorkOS access token |
User::get_workos_id( $user_id = 0 ) |
string |
WorkOS user_... identifier, or '' |
User::get_access_token( $user_id = 0 ) |
string |
Current WorkOS access token (treat as opaque) |
User::get_refresh_token( $user_id = 0 ) |
string |
Stored WorkOS refresh token |
User::get_session_id( $user_id = 0 ) |
string |
WorkOS sid claim for the active session |
User::get_organization_id( $user_id = 0 ) |
string |
WorkOS organization id pinned to the user |
User::snapshot( $user_id = 0 ) |
array |
All of the above in one predictable payload |
All methods accept 0 (or omitted) to target the currently-authenticated
user. All return empty strings / false safely when no user is available,
so there's no need to null-check get_current_user_id() first.
The meta keys are also exposed as constants (User::META_WORKOS_ID,
META_ACCESS_TOKEN, etc.) for callers that need them in SQL queries or
REST schemas.
workos_is_sso_user( $user_id = 0 ); // bool — is the user linked?
workos_has_active_session( $user_id = 0 ); // bool — is a session stored?
workos_get_user_id( $user_id = 0 ); // string — WorkOS user id
workos_get_access_token( $user_id = 0 ); // string — current access tokenis_sso()remains true after the user logs out. Use it when you want to know whether an account was ever provisioned via WorkOS.has_active_session()flips to false onwp_logout(the plugin clears the access token server-side). Use it when you want to know whether a request is running under a live WorkOS session.
use WorkOS\User;
add_filter( 'my_plugin_response', function ( array $data ): array {
if ( ! User::is_sso() ) {
return $data;
}
$data['workos'] = [
'linked' => true,
'active_session' => User::has_active_session(),
'workos_user_id' => User::get_workos_id(),
'organization_id' => User::get_organization_id(),
];
return $data;
} );Or with the function shortcuts inside a non-namespaced file:
function my_plugin_add_workos_data( array $data ): array {
if ( ! workos_has_active_session() ) {
return $data;
}
$data['workos_user_id'] = workos_get_user_id();
// ...
return $data;
}Note: neither helper verifies that the access token is still valid
against WorkOS's JWKS. If you need authoritative session state (e.g.
before authorizing a sensitive action), verify via
workos()->api()->verify_access_token( $token ).
Filter the full role-to-redirect entry map from settings. Each entry is an array with url (string) and first_login_only (bool). Allows adding, removing, or overriding entries programmatically.
Parameters:
array $map— Associative array of WordPress role slug to redirect entry (['url' => string, 'first_login_only' => bool]).
Example:
add_filter( 'workos_redirect_urls', function ( $map ) {
$map['subscriber'] = [ 'url' => '/welcome', 'first_login_only' => true ];
return $map;
} );Filter the final redirect URL for a specific user. Return an empty string to skip the role-based redirect.
Parameters:
string $url— The role-based redirect URL (empty if no match).WP_User $user— The authenticated user.string $role— The user's primary WordPress role.bool $is_first_login— Whether this is the user's first login via WorkOS.
Example:
add_filter( 'workos_redirect_url', function ( $url, $user, $role, $is_first_login ) {
if ( $role === 'editor' ) {
return '/editor-guide';
}
return $url;
}, 10, 4 );Whether the role-based redirect should apply at all for this request. Return false to skip entirely.
Parameters:
bool $should_apply— Whether to apply the role-based redirect (defaulttrue).WP_User $user— The authenticated user.string $requested_redirect— The current redirect URL.
Example:
add_filter( 'workos_redirect_should_apply', function ( $should_apply, $user ) {
// Never redirect administrators.
if ( in_array( 'administrator', $user->roles, true ) ) {
return false;
}
return $should_apply;
}, 10, 2 );Whether the current redirect_to value is considered "explicit" (user-initiated). By default, any redirect_to that is not admin_url() or empty is treated as explicit, meaning the role-based redirect is skipped in favor of the user's intended destination.
Parameters:
bool $is_explicit— Whether the redirect is explicit.string $redirect_to— The redirect URL.WP_User $user— The authenticated user.
Override the per-entry "first login only" setting programmatically.
Parameters:
bool $first_login_only— Whether to redirect only on first login.string $role— The user's primary WordPress role.WP_User $user— The authenticated user.
Filter the full role-to-logout-redirect URL map from settings. Unlike login redirects, each entry is a simple URL string (no first_login_only option).
Parameters:
array $map— Associative array of WordPress role slug to logout redirect URL (string).
Example:
add_filter( 'workos_logout_redirect_urls', function ( $map ) {
$map['subscriber'] = '/goodbye';
$map['editor'] = '/editor-farewell';
return $map;
} );Filter the final logout redirect URL for a specific user. Return an empty string to skip the role-based logout redirect.
Parameters:
string $url— The role-based logout redirect URL (empty if no match).WP_User $user— The authenticated user.string $role— The user's primary WordPress role.
Example:
add_filter( 'workos_logout_redirect_url', function ( $url, $user, $role ) {
if ( $role === 'administrator' ) {
return '/admin-logged-out';
}
return $url;
}, 10, 3 );Whether the role-based logout redirect should apply at all for this request. Return false to skip entirely.
Parameters:
bool $should_apply— Whether to apply the role-based logout redirect (defaulttrue).WP_User $user— The authenticated user.string $redirect_to— The current logout redirect URL.
Example:
add_filter( 'workos_logout_redirect_should_apply', function ( $should_apply, $user ) {
// Never redirect administrators on logout.
if ( in_array( 'administrator', $user->roles, true ) ) {
return false;
}
return $should_apply;
}, 10, 2 );Fires when a brand-new WordPress user is created via WorkOS authentication. Does NOT fire for email-match auto-links (existing users matched by email).
Parameters:
int $user_id— WordPress user ID.array $workos_user— WorkOS user data array.
Fires just before a role-based login redirect is applied.
Parameters:
string $url— The redirect URL.WP_User $user— The authenticated user.bool $is_first_login— Whether this is the user's first login via WorkOS.
Fires when a role-based login redirect is skipped. Useful for logging or debugging redirect behavior.
Parameters:
WP_User $user— The authenticated user.string $reason— Reason the redirect was skipped. One of:filtered_out,explicit_redirect,not_first_login,no_matching_role_url.
Fires just before a role-based logout redirect is applied.
Parameters:
string $url— The logout redirect URL.WP_User $user— The authenticated user.
Fires when a role-based logout redirect is skipped.
Parameters:
WP_User $user— The authenticated user.string $reason— Reason the logout redirect was skipped. One of:filtered_out,no_matching_role_url.
Fires after a Login Profile is created or updated through the admin REST
API (/wp-json/workos/v1/admin/profiles).
Parameters:
WorkOS\Auth\AuthKit\Profile $profile— The saved profile (post-validation, with assigned ID).
Fires after a Login Profile is deleted through the admin REST API. Useful
for invalidating caches or rewrite-rule signatures keyed on profile data
(this is exactly how Auth\AuthKit\FrontendRoute knows to rebuild its
custom-path rewrites).
Parameters:
WorkOS\Auth\AuthKit\Profile $profile— The profile that was deleted.
The plugin creates four custom tables on activation:
| Table | Purpose |
|---|---|
{prefix}_workos_organizations |
Cached WorkOS organization data (name, slug, domains) |
{prefix}_workos_org_memberships |
User-to-organization memberships with roles |
{prefix}_workos_org_sites |
Organization-to-site mapping (multisite) |
{prefix}_workos_activity_log |
Local activity log for authentication and sync events |
User linking is stored in standard WordPress usermeta (_workos_user_id, _workos_org_id, _workos_last_synced_at, _workos_deactivated).
All commands are registered under the wp workos namespace.
# Show plugin configuration and health
wp workos status
# Output as JSON
wp workos status --format=jsonDisplays: environment, API key (masked), client ID, organization ID, environment ID, enabled status, login mode, database version, and plugin version. The source column shows whether each value comes from a constant or the database.
# Get a user with WorkOS metadata
wp workos user get <id> [--by=<id|email|workos_id>] [--format=<format>]
# List users with WorkOS link status
wp workos user list [--linked] [--unlinked] [--role=<role>] [--format=<format>] [--fields=<fields>]
# Get user IDs for piping to other commands
wp workos user list --unlinked --format=ids
# Link a WP user to a WorkOS user (validates via API)
wp workos user link <wp_user_id> <workos_user_id>
# Remove WorkOS link from a user
wp workos user unlink <wp_user_id> [--yes]
# Sync a single user: push to WorkOS (default) or pull from WorkOS
wp workos user sync <wp_user_id> [--direction=<push|pull>]
# Import a single WorkOS user into WordPress
wp workos user import <workos_user_id> [--porcelain] [--yes]Examples:
# Find a user by email and show their WorkOS metadata
wp workos user get admin@example.com --by=email
# Look up a user by their WorkOS ID
wp workos user get user_01HXYZ --by=workos_id --format=json
# List all users that haven't been synced to WorkOS yet
wp workos user list --unlinked --role=subscriber
# Pull latest profile data from WorkOS for a linked user
wp workos user sync 42 --direction=pull
# Import a WorkOS user and get just the WP user ID
wp workos user import user_01HXYZ --porcelain --yes# List local organizations
wp workos org list [--source=<local|remote>] [--format=<format>]
# Get a single organization
wp workos org get <id> [--by=<id|workos_id|remote>] [--format=<format>]
# Sync an organization from WorkOS API to local database
wp workos org sync <workos_org_id>
# List organization members
wp workos org members <id> [--by=<id|workos_id>] [--format=<format>]
# Add a user to a local organization
wp workos org add-member <org_id> <user_id> [--role=<role>]
# Remove a user from a local organization
wp workos org remove-member <org_id> <user_id> [--yes]Examples:
# List organizations from the WorkOS API
wp workos org list --source=remote --format=json
# Fetch an org directly from WorkOS without needing a local record
wp workos org get org_01HXYZ --by=remote
# Sync an organization and view its members
wp workos org sync org_01HXYZ
wp workos org members org_01HXYZ --by=workos_id
# Add a user to an org as admin
wp workos org add-member 1 42 --role=adminAll bulk commands support --dry-run to preview changes, --yes to skip confirmation, --limit to cap the number of items processed, and display progress bars during execution.
# Push all unlinked WP users to WorkOS
wp workos sync push [--role=<role>] [--limit=<n>] [--dry-run] [--yes]
# Re-sync all linked users from WorkOS
wp workos sync pull [--limit=<n>] [--dry-run] [--yes]
# Import WorkOS users into WordPress
wp workos sync import [--organization_id=<id>] [--limit=<n>] [--dry-run] [--yes]
# Import all organizations from WorkOS
wp workos sync orgs [--limit=<n>] [--dry-run] [--yes]Examples:
# Preview what a full user push would do
wp workos sync push --dry-run
# Push only subscribers, 50 at a time
wp workos sync push --role=subscriber --limit=50 --yes
# Re-sync all linked users from WorkOS
wp workos sync pull --yes
# Import the first 10 WorkOS users from a specific organization
wp workos sync import --organization_id=org_01HXYZ --limit=10 --yes
# Import all organizations
wp workos sync orgs --yesOutput format: All bulk commands report a summary on completion:
Success: 45 synced, 2 failed, 3 skipped.
Non-fatal errors are shown as warnings during execution and counted in the summary.
All commands that display data support these output formats:
| Flag | Format |
|---|---|
--format=table |
ASCII table (default) |
--format=json |
JSON array |
--format=yaml |
YAML |
--format=csv |
CSV |
--format=ids |
Space-separated IDs (user list only) |
- PHP 7.4+
- Composer
- Node.js 20+
- bun
- slic (for running tests)
composer install
bun install # Pulls @wordpress/scripts, TypeScript, @types/react
bun run build # Build JS/CSS assetsThe React shell (src/js/authkit/) and the admin Profile editor
(src/js/admin-profiles/) are TypeScript. @wordpress/scripts v30
transpiles .ts / .tsx natively via its default babel preset; no extra
build config is required. Type-check with:
bun run lint:ts # tsc --noEmit against src/js/authkit + src/js/admin-profilesShared types live in src/js/authkit/types.ts and mirror
Profile::to_array() from src/WorkOS/Auth/AuthKit/Profile.php.
# Using slic (Docker-based)
cd wp-content/plugins
slic here
slic use integration-workos
slic run wpunit
# Using Composer
composer test:wpunitcomposer lint # PHP (check)
composer lint:fix # PHP (auto-fix)
bun run lint:ts # TypeScriptPHPStan runs at level 5 against src/, integration-workos.php,
and uninstall.php. The config lives in phpstan.neon.dist and is
enforced by the PHPStan GitHub Actions job — PRs to main cannot
merge while it's red.
composer phpstan # Run analysis (--memory-limit=1G)
composer phpstan:baseline # Generate phpstan-baseline.neon (fix-everything policy: do not commit)The stack:
phpstan/phpstan^2 — analyzerszepeviktor/phpstan-wordpress— WordPress core stubs + WP-aware inference (handlesapply_filters,wp_remote_request, hook signatures)php-stubs/wp-cli-stubs—WP_CLI,WP_CLI_Command,WP_CLI\Formatter, etc. for thesrc/WorkOS/CLI/*commandsphpstan/extension-installer— auto-registers extension neon files
phpstan/stubs.php declares the WORKOS_* constants that
Plugin::init() defines at runtime so PHPStan can resolve them at
parse time. Strauss-prefixed WorkOS\Vendor\… classes are picked up
automatically via vendor/autoload.php in scanFiles.
Policy: no baseline. Findings must be fixed in the PR that
introduces them. The composer phpstan:baseline script exists as a
safety hatch, but the resulting file should not be committed without
discussion.
The plugin uses a DI container (di52) with a feature-controller pattern:
integration-workos.php # Entry point
src/WorkOS/Plugin.php # Bootstrap, container init, activation hook
src/WorkOS/Controller.php # Main controller, registers feature controllers
src/WorkOS/Config.php # Centralized config with constant overrides
src/WorkOS/
Admin/
Controller.php # Settings UI, user list, onboarding, diagnostics
LoginProfiles/
Controller.php # Admin controller for Login Profile editor
AdminPage.php # Renders the React admin mount point
RestApi.php # /wp-json/workos/v1/admin/profiles CRUD
Auth/
Controller.php # Login, registration, password reset, redirects
Login.php # Legacy AuthKit redirect + headless flows
AuthKit/ # Custom AuthKit (React shell) — see below
Controller.php # Wires everything, registers CPT + takeover
Profile.php # Immutable Profile value object
ProfileRepository.php # CPT-backed CRUD
ProfileRouter.php # Rule-based profile resolution
LoginCompleter.php # Shared post-auth finalizer (entitlement + MFA)
LoginTakeover.php # wp-login.php takeover (action=login only) + default-profile custom-path bounce
LoginRedirector.php # Already-signed-in visitor redirect + forward_query_args helper
FrontendRoute.php # /workos/login/{profile} + per-profile custom_path rewrites
Shortcode.php # [workos:login] shortcode
Renderer.php # HTML shell + bundle enqueue + logo fallback chain
Nonce.php # Profile-scoped CSRF nonces
RateLimiter.php # Per-IP / per-email transient buckets
Radar.php # Site-key resolution + request-header extraction
ModeSyncer.php # Keeps global login_mode option in sync with the default profile's mode
REST/
Controller.php # REST controller (registers Auth and TokenAuth)
TokenAuth.php # REST API Bearer token authentication
Auth/ # Public /wp-json/workos/v1/auth/* endpoints
Controller.php # Wires all endpoint classes
BaseEndpoint.php # Shared profile + nonce + rate-limit + Radar
Password.php # password/authenticate + reset/{start,confirm}
MagicCode.php # magic/{send,verify}
Session.php # nonce + session/{refresh,logout}
Signup.php # signup/{create,verify}
Invitation.php # invitation/{token} + invitation/accept
OAuth.php # oauth/authorize-url
Mfa.php # mfa/{challenge,verify,factors,…}
Sync/Controller.php # UserSync, RoleMapper, DirectorySync, AuditLog
Webhook/Controller.php # Webhook receiver + signature verification
Organization/Controller.php # Organization management, entitlement gate
CLI/Controller.php # WP-CLI commands
UI/Controller.php # Legacy login button (shortcode, block, widget)
ActivityLog/Controller.php # Local activity logging
src/js/
authkit/ # Custom AuthKit React shell (TypeScript + TSX)
index.tsx # Mount + data-* hydration
App.tsx # Step machine
api.ts # Fetch client w/ nonce + refresh + Radar
flows.tsx # 11 flow components (password, magic, mfa, ...)
ui.tsx # 11 primitives (Button, Input, Card, ...)
radar.ts # WorkOS Radar SDK loader
redirect.ts # forwardQueryArgs() — strips internals, mirrors LoginRedirector allowlist
slots.tsx # SlotFill slot name constants (10 slots)
types.ts # Shared interfaces
styles.css # Scoped styles (CSS vars)
admin-profiles/
index.tsx # Admin Login Profile editor (CRUD)
styles.css # Scoped admin styles
Each controller extends Contracts\Controller and implements isActive()
for conditional activation (e.g., Admin\Controller only activates in
is_admin(), CLI\Controller only activates under WP_CLI). The new
Auth\AuthKit\Controller and REST\Auth\Controller are always active —
the former because the CPT and wp-login.php takeover must boot on every
request, the latter because anonymous visitors need to reach the auth
endpoints.



