Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ tests/_support/_generated/
# OS
.DS_Store
Thumbs.db
/.claude/
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ Per-environment constants (take priority over generic):
| `src/WorkOS/Auth/PasswordResetAdmin/Shortcode.php` | `[workos:password-reset]` — toggles between admin-of-other (`user="id-or-email"`) and self-service (no `user`) modes based on its attributes. |
| `src/WorkOS/Auth/PasswordResetAdmin/Assets.php` | Registers `workos-admin-password-reset` JS/CSS handles + localizes `workosPasswordReset` (REST URL, `wp_rest` nonce, masked-email-aware UI strings). Auto-enqueues on `users.php` / `user-edit.php` / `profile.php`. |
| `src/js/admin-password-reset/index.ts` | Delegated `.workos-pwreset-trigger` click handler — POSTs to the admin endpoint and surfaces a transient admin notice. Same handler powers every trigger surface (WP Users row, user-edit panel, shortcode, WorkOS Users admin page row). |
| **Auth — Change Email** ([`docs/change-email.md`](docs/change-email.md)) | |
| `src/WorkOS/Auth/ChangeEmail/Controller.php` | DI controller — wires REST endpoints, JS/CSS, row action, profile panel, shortcode, and the frontend confirm route. Honors the `change_email_enabled` setting + `workos_change_email_enabled` filter. |
| `src/WorkOS/Auth/ChangeEmail/RestApi.php` | Three endpoints: `POST /workos/v1/users/{id}/email-change` (initiate, `edit_user` cap, per-IP + per-user rate limits, enumeration-safe conflict response), `…/email-change/confirm` (token-gated, race re-check, transient-guarded WorkOS + WP commit), `…/email-change/cancel` (cancel-token *or* `edit_user` cap). Logs `email_change.*` events. |
| `src/WorkOS/Auth/ChangeEmail/PendingChange.php` | Stores the pending record as `_workos_pending_email_change` user_meta — only hashes (HMAC-SHA256 + `wp_salt('auth')`), never plaintext tokens. Single-use: cleared on confirm/cancel/expiry. |
| `src/WorkOS/Auth/ChangeEmail/TokenFactory.php` | `wp_generate_password()`-backed token generator + `hash_hmac()` storage + `hash_equals()` verify. |
| `src/WorkOS/Auth/ChangeEmail/ConflictResolver.php` | Enforces `change_email_conflict_policy`: `block` (default), `allow_orphan` (gated by `_workos_user_id`, posts, comments, last-login window — defaults to 90 days, filterable via `workos_change_email_orphan_max_inactive_days`), `merge_request` (rejects + fires `workos_change_email_merge_requested` for Issue 2). Fires `workos_change_email_conflict_detected` on any collision. |
| `src/WorkOS/Auth/ChangeEmail/Notifier.php` | Three transactional emails via `wp_mail()`: verification to the new address, "change requested" cancel-link to the old address (suppressed by `change_email_notify_old_address=false`), post-commit confirmation to the old address. |
| `src/WorkOS/Auth/ChangeEmail/RowActions.php` | "Change email" row action under the WorkOS column on `wp-admin/users.php` (priority 11, sits next to "Send password reset"). |
| `src/WorkOS/Auth/ChangeEmail/UserProfilePanel.php` | "Change Email" panel + trigger button on the user-edit / profile screen (priority 21, after the password-reset panel). |
| `src/WorkOS/Auth/ChangeEmail/Shortcode.php` | `[workos:change-email]` — self-service form when no `user` attribute; admin-of-other form when `user="id-or-email"`. |
| `src/WorkOS/Auth/ChangeEmail/FrontendConfirmRoute.php` | Rewrite rule for the configurable confirm path (default `/workos/change-email/`, settable via `change_email_confirm_path`). `template_redirect` renders `templates/change-email/confirm-page.php`, which enqueues the JS bundle that POSTs to the confirm/cancel endpoint. |
| `src/WorkOS/Auth/ChangeEmail/Assets.php` | Registers `workos-admin-change-email` (admin row/panel/shortcode trigger) + `workos-change-email-confirm` (frontend confirm page) JS/CSS handles. |
| `src/js/admin-change-email/index.ts` | Delegated `.workos-change-email-trigger` click handler — prompts for the new email (standalone button) or reads from `.workos-change-email-input` (shortcode form), POSTs to the initiate endpoint. |
| `src/js/change-email-confirm/index.ts` | Frontend confirm-page logic: reads token + user_id from URL, picks confirm vs cancel based on `?action=cancel`, POSTs, renders success / error. |
| `src/WorkOS/Email/Mailer.php` | Small `wp_mail()` wrapper shared by the change-email Notifier — locates templates (with theme override at `wp-content/themes/{theme}/integration-workos/{template}.php`), sets HTML headers + a filterable from-address, and exposes `workos_email_body`, `workos_email_subject`, `workos_email_headers` filters. |
| `templates/change-email/*.php` | Email + confirm-page templates (verification, old-address notice, confirmation notice, confirm page). |
| `src/WorkOS/Auth/Redirect.php` | Role-based login redirects |
| `src/WorkOS/Auth/LogoutRedirect.php` | Role-based logout redirects |
| **Auth — Custom AuthKit (React shell)** | |
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

## [1.0.6] - Unreleased

### Added

- **Change email (WorkOS-verified, conflict-guarded)** (#22) — self-service and admin-triggered email-change flow.
- Self-service `[workos:change-email]` shortcode.
- Admin "Change email" row action under the **WorkOS column** on `wp-admin/users.php`; "Change Email" panel on the user-edit / profile screen.
- WP-side verification: hashed (HMAC-SHA256 + `wp_salt('auth')`) confirm + cancel tokens stored as `_workos_pending_email_change` user_meta, validated with `hash_equals`, single-use, expiry-bounded. (WorkOS's `email_verification` endpoints can't verify a *pending* address.)
- Three new REST endpoints: `POST /workos/v1/users/{id}/email-change` (initiate, capability-gated, per-IP + per-user rate-limited, enumeration-safe conflict response), `…/email-change/confirm` (race re-checked, transient-guarded WorkOS + WP commit, rollback on partial failure), `…/email-change/cancel` (cancel-token or `edit_user` cap).
- Configurable conflict policy: `block` (default), `allow_orphan` (gated by `_workos_user_id`, posts, comments, last-login window), `merge_request` (rejects today, fires `workos_change_email_merge_requested` for the future merge feature).
- `wp_mail()`-backed verification, old-address cancel-link notice (opt-out via `change_email_notify_old_address`), and post-commit confirmation. Templates under `templates/change-email/` and overridable from a theme.
- Frontend confirm route under a configurable path (default `/workos/change-email/`, settable via `change_email_confirm_path`).
- UserSync race guard: `_workos_email_change_in_progress_<user_id>` transient short-circuits `handle_user_updated()` while the confirm handler is mid-commit.
- 8 new filters (`workos_change_email_enabled`, `…/conflict_policy`, `…/token_lifetime`, `…/can_initiate`, `…/notify_old_address`, `…/orphan_max_inactive_days`, plus `workos_email_subject|body|headers`) and 5 actions (`…/initiated`, `…/confirmed`, `…/cancelled`, `…/conflict_detected`, `…/merge_requested`).
- 7 new activity-log event types: `email_change.initiated|confirmed|cancelled|expired|conflict_blocked|commit_failed|admin_bypass`.
- 40 new WPUnit tests across 6 suites under `tests/wpunit/ChangeEmail*Test.php`.
- See [`docs/change-email.md`](docs/change-email.md).

## [1.0.5] - 2026-05-18

### Added
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Enterprise identity management for WordPress powered by [WorkOS](https://workos.
- **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 (two-field new-password form with `wp.passwordStrength.meter` gating, optional auto-login on success, post-reset `redirect_url` validated same-host)
- **Admin-triggered password reset** — send a WorkOS reset email on behalf of any linked user via `POST /wp-json/workos/v1/admin/users/{id}/password-reset` (gated by `edit_user($id)` so the same route covers self-service). Surfaced as a row action under the **WorkOS column** on `wp-admin/users.php`, a button on the user-edit screen, a per-row button on the WorkOS Users admin page, and a `[workos:password-reset]` shortcode. Successful sends are audited as `password_reset.admin_sent` in the activity log; emails point at the in-site React shell (`/workos/login/{slug}?token=…&redirect_to=…`) and the new password is mirrored to the linked WP user so `?fallback=1` / `wp_authenticate` / REST app passwords stay in sync. See [`docs/password-reset.md`](docs/password-reset.md).
- **Change email** — WorkOS-verified, conflict-guarded email-change flow. Self-service via `[workos:change-email]` shortcode; admin via a row action under the **WorkOS column** on `wp-admin/users.php` and a panel on the user-edit screen. WP-side hashed token sent to the new address; cancel-link notice sent to the old address; configurable conflict policy (`block`, `allow_orphan`, `merge_request`) keeps the new email from silently overwriting another local WP user. Confirm commits to WorkOS first (via `update_user`) and then mirrors into WP, with a 60-second in-progress transient that short-circuits the webhook fan-back. See [`docs/change-email.md`](docs/change-email.md).
- **Skeleton placeholders** on every AuthKit surface (wp-login.php takeover, `/workos/login/{profile}`, shortcode) — pre-hydration markup from PHP plus a React `FlowSkeleton` during bootstrap, mirroring the real card heights so swap-in is a flicker not a jump.
- **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_to` glob / `referrer_host` / `user_role` matchers pick the right profile per request
Expand Down Expand Up @@ -177,7 +178,11 @@ full developer guide on injecting React elements (SlotFill), enqueuing
per-profile CSS/JS, and the available PHP filters. For password-reset
integrations (admin-triggered, self-service, shortcode, redirect_url
policy, what-not-to-do), see
[`docs/password-reset.md`](docs/password-reset.md).
[`docs/password-reset.md`](docs/password-reset.md). For the
WorkOS-verified change-email flow (self-service + admin row action +
configurable conflict policy + WP-side token verification + cancel
link to old address), see
[`docs/change-email.md`](docs/change-email.md).

### Custom paths

Expand Down
Loading
Loading