You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add Sessions UI surfaces for WordPress admins and end users to list and revoke active WorkOS sessions. Admins gain a "WorkOS Sessions" panel on the user-edit screen; end users get a [workos:user-sessions] shortcode and matching block to manage their own sessions. Webhook-driven Activity Log entries track session creation and revocation. This ships as part of 1.1.0 (feature F2), following Webhook event coverage expansion (F3) and preceding the Admin Portal embed (F1).
Background / current state
The plugin already stores _workos_session_id in user meta (src/WorkOS/User.php:70) and has a revoke_session() method on the API client (src/WorkOS/Api/Client.php:802). However:
No admin UI — admins cannot see what sessions a user has or revoke them from WordPress; must leave to the WorkOS dashboard.
No end-user UI — users have no "Security" or "Sessions" page to revoke devices they no longer use.
No session lifecycle logging — the Activity Log (src/WorkOS/ActivityLog/EventLogger.php) subscribes only to workos_user_authenticated, wp_logout, and wp_login_failed (ActivityLog/Controller.php:25-32). Session creation/revocation events are not captured.
No list endpoint — the Client.php wraps only revoke_session(). A list_user_sessions() method must be added to call WorkOS's GET /user_management/sessions?user_id=….
UserProfile.php (lines 69-107) already renders WorkOS metadata, memberships, and recent events; a Sessions panel slots below the Recent Events section.
Goals & acceptance criteria
Admin user-edit screen displays "WorkOS Sessions" panel listing active sessions (sid, IP, user agent, created_at, last_active_at) for the edited user, with per-row Revoke button.
Revoke button calls POST /wp-json/workos/v1/sessions/{session_id}/revoke with permission check (self or admin).
End-user shortcode [workos:user-sessions] and block workos/user-sessions render the same list for the current user; they can revoke only their own sessions.
Session revocation is immediately reflected in the UI (no stale cache).
Revoking the user's current session triggers a 401 on the next API request; React component redirects to login.
session.created and session.revoked webhook events fire do_action( "workos_webhook_{type}", $event ) (via F3 Webhook expansion) and are logged to Activity Log with actor_id tracked (admin revokes show actor ≠ target).
src/WorkOS/Sessions/Manager.php — Instance class (no static methods per project convention) wrapping list + revoke calls with 60-second per-user transient caching.
src/WorkOS/Sessions/RestApi.php — Three REST endpoints: GET /wp-json/workos/v1/sessions/me, GET /wp-json/workos/v1/admin/users/{id}/sessions (manage_options), POST /wp-json/workos/v1/sessions/{session_id}/revoke (self-or-admin).
src/WorkOS/UI/SessionsBlock.php — Registers the workos/user-sessions block; renders the Sessions React component.
src/WorkOS/UI/SessionsShortcode.php — Registers [workos:user-sessions] shortcode; renders the Sessions React component.
src/js/sessions/SessionsPanel.tsx (or .ts) — Single React component consumed by admin-side (mode="admin", current user id passed as prop) and front-end (mode="self", always current user). Calls REST endpoints, handles 401 redirect, displays session list with revoke buttons, pagination.
Files to change
src/WorkOS/Api/Client.php — Add method public function list_user_sessions( string $user_id, array $params = [] ) calling GET /user_management/sessions?user_id=…. Pattern matches existing methods like list_auth_factors() (lines 505-507) and list_organization_memberships() (lines 701-703). Verify and match existing error handling and response envelope structure.
src/WorkOS/Admin/UserProfile.php — Extend the render() method (line 69) to include a Sessions panel section below Recent Events (line 106). Fetch sessions using the Manager with caching (existing pattern at line 104). Render a table with sid, IP, user agent, created_at, last_active_at, revoke button. Call the endpoint for the edited user, not the current user.
src/WorkOS/UI/Controller.php — Register SessionsBlock and SessionsShortcode in doRegister() (line 24) alongside existing Block and Ajax registrations. Enqueue Sessions assets (CSS, JS bundle) conditionally when the block or shortcode is present, matching the pattern for login-button (lines 59-112).
src/WorkOS/ActivityLog/Controller.php — Add two new action hooks: add_action( 'workos_webhook_session.created', [ $this, 'log_session_created' ], 10, 1 ) and add_action( 'workos_webhook_session.revoked', [ $this, 'log_session_revoked' ], 10, 1 ) in doRegister(). Implement log_session_created() and log_session_revoked() methods to call EventLogger::log() with appropriate metadata (session_id, user_id extracted from webhook, actor_id if revoked by admin).
src/WorkOS/Admin/DiagnosticsPage.php — Add a Sessions API check: call Manager::list_user_sessions() with the current user and report reachability or error. Integrate into run_all_checks() (line 161).
REST endpoints
GET /wp-json/workos/v1/sessions/me (requires auth, no user_id param)
Purpose: List sessions for the current user (authenticated via cookie or Bearer token).
Error: 403 if user lacks permission; 400 if body invalid; 500 on API error.
Side effect: If the current request is authenticating via this session (Bearer token), subsequent requests 401 and the React component redirects to login.
Data model (options / user_meta / tables)
No new database schema. Uses existing _workos_session_id meta key (User.php:70). New activity log entries use the existing wp_workos_activity_log table (EventLogger.php:51):
event_type: 'session_created' or 'session_revoked'
user_id: WP user ID (the user whose session was created/revoked).
metadata: JSON object:
{ "session_id": "...", "ip_address": "...", "user_agent": "...", "created_at": "...", "actor_id": int (only for revoked), "was_current": bool (only for revoked) }
Session Manager uses transient cache:
Key: workos_sessions_<md5(user_id)>
TTL: 60 seconds
Value: Webhook response array (list of sessions).
Hooks & filters (new actions/filters this feature adds, named)
No new filters defined by this feature. The feature consumes existing actions:
workos_webhook_session.created — Fired by Webhook/Receiver.php when F3 expands the allowlist. Sessions/Controller listens.
workos_webhook_session.revoked — Fired by Webhook/Receiver.php when F3 expands the allowlist. Sessions/Controller listens.
Activity Log also listens to the catch-all workos_webhook (WorkOS\Webhook\Receiver.php:134), already dispatched for all events post-F3.
WorkOS API
GET /user_management/sessions
Query params: user_id (required), limit (default 25, max 100), after (cursor for pagination).
Error: 404 if session not found; 401 if API key invalid.
(Both endpoints already wrapped at Client.php:802 (revoke_session) and new list_user_sessions; no new wrapper needed beyond list_user_sessions.)
Edge cases & security
Current session revocation: The WorkOS API may not distinctly flag the authenticated user's current session. Solution: compare _workos_session_id user meta to each session id; mark the match as "This device" in the UI.
401 on current session revoke: If a user revokes their active session via Bearer token, WorkOS immediately returns 401 on the next request. React component must catch 401 in the sessions list fetch and redirect to the login page (or trigger a WP-side logout) rather than retrying.
Admin revokes user's session: Activity Log entry must record actor_id (the admin's WP user ID) separately from user_id (the target). EventLogger.log() receives metadata { "actor_id": get_current_user_id() } when called from the REST endpoint's revoke handler.
Rate limiting: WorkOS API enforces per-organization rate limits. The Manager caches 60 seconds per user to minimize requests during admin session browsing.
Enumeration: REST endpoint /admin/users/{id}/sessions requires manage_options so non-admins cannot enumerate sessions per user. /sessions/me is limited to the authenticated user by REST/TokenAuth.php.
CSRF: REST endpoints use WordPress nonce verification by default (REST framework automatically validates unless permission_callback is omitted or a custom auth is used). Bearer token requests skip nonce but are auth-protected by REST/TokenAuth.
Pagination: If the UI shows 25 most-recent sessions and the endpoint supports cursor-based pagination, implement "Load more" conditionally (defer if pagination adds scope). First load shows 25 without UI for more; if list_metadata.after is present, show a "Load more" button.
No logout-on-revoke: Revoking a session does not log the user out of WordPress; it only invalidates the WorkOS session. The user's WP cookie remains valid. Optional: add a wp_logout hook in Sessions/Controller that calls Manager::revoke_current_session() if desired (out of scope unless explicitly implemented).
Testing
WPUnit (/slic)
Create the following test files under tests/wpunit/:
tests/wpunit/Sessions/ManagerTest.php — Tests the Manager class:
Result: OK if list is returned; error message if 401, timeout, or empty.
Activity Log event types:
session_created — Fired by the webhook listener on workos_webhook_session.created. Metadata includes session_id, ip_address, user_agent, created_at.
session_revoked — Fired by the webhook listener on workos_webhook_session.revoked or by the REST revoke endpoint. Metadata includes session_id, actor_id (who revoked), was_current (bool).
Add a subsection "User Sessions & Revocation" with:
Overview of the three surfaces (admin panel, shortcode, block).
Example of listening to session webhook events: add_action( 'workos_webhook_session.created', function( $event ) { ... } );
REST endpoint examples (cURL/JavaScript).
Explanation of the current session / "This device" behavior.
Update CHANGELOG.md for 1.1.0:
Add line: "Add Sessions UI: admins can view and revoke user sessions from the user-edit page; end users get [workos:user-sessions] shortcode and block."
Add line: "Security: session revocation is now fully audited in Activity Log and available as a webhook event."
Out of scope
Multi-device login limits (e.g. "max 5 active sessions per user") — deferred to future release; this version provides the UI only.
Logout on revoke: The optional wp_logout hook integration (logout of WordPress when a WorkOS session is revoked) is out of scope but documented above for future inclusion.
Custom session naming (e.g. "Work Laptop") — WorkOS API does not support user-assigned labels; deferred.
Session export / CSV — deferred to polish backlog.
Geo-location or device type detection — WorkOS does not provide; out of scope.
Dependencies & sequencing
Within 1.1.0, this feature depends on:
F3: Webhook event coverage must be merged first. F3 expands the Webhook/Receiver.php allowlist (line 100-117) to include session.created and session.revoked. Without F3, webhook logging in this feature does not fire. Sequencing: F3 → F2 → F1.
Sibling features in 1.1.0:
F1: Admin Portal embed — independent; can ship in parallel once both are code-complete.
F3: Webhook event coverage — must be merged before this feature's webhook tests run; otherwise deferred to after F3.
External dependencies:
WorkOS API endpoints GET /user_management/sessions and POST /user_management/sessions/{id}/revoke (already available in production).
WordPress 5.0+ for REST API, WP_User, and transients.
React 18+ for front-end component (already required by login-button block).
No breaking changes to public APIs — Sessions/Manager and Sessions/RestApi are new; existing User.php, Client.php, and ActivityLog/EventLogger are backward-compatible.
1.1.0 milestone navigation
Recommended landing order — #31 → #30 → #29; #28 is independent and may land any time. Each lands as soon as it is green (no monolithic 1.1.0 PR).
Summary
Add Sessions UI surfaces for WordPress admins and end users to list and revoke active WorkOS sessions. Admins gain a "WorkOS Sessions" panel on the user-edit screen; end users get a
[workos:user-sessions]shortcode and matching block to manage their own sessions. Webhook-driven Activity Log entries track session creation and revocation. This ships as part of 1.1.0 (feature F2), following Webhook event coverage expansion (F3) and preceding the Admin Portal embed (F1).Background / current state
The plugin already stores
_workos_session_idin user meta (src/WorkOS/User.php:70) and has arevoke_session()method on the API client (src/WorkOS/Api/Client.php:802). However:workos_user_authenticated,wp_logout, andwp_login_failed(ActivityLog/Controller.php:25-32). Session creation/revocation events are not captured.revoke_session(). Alist_user_sessions()method must be added to call WorkOS'sGET /user_management/sessions?user_id=….UserProfile.php (lines 69-107) already renders WorkOS metadata, memberships, and recent events; a Sessions panel slots below the Recent Events section.
Goals & acceptance criteria
POST /wp-json/workos/v1/sessions/{session_id}/revokewith permission check (self or admin).[workos:user-sessions]and blockworkos/user-sessionsrender the same list for the current user; they can revoke only their own sessions.session.createdandsession.revokedwebhook events firedo_action( "workos_webhook_{type}", $event )(via F3 Webhook expansion) and are logged to Activity Log with actor_id tracked (admin revokes show actor ≠ target)./sessions/me,/admin/users/{id}/sessions,/sessions/{session_id}/revoke) validate permissions correctly.Technical design
Files to add
src/WorkOS/Sessions/Manager.php— Instance class (no static methods per project convention) wrapping list + revoke calls with 60-second per-user transient caching.src/WorkOS/Sessions/RestApi.php— Three REST endpoints:GET /wp-json/workos/v1/sessions/me,GET /wp-json/workos/v1/admin/users/{id}/sessions(manage_options),POST /wp-json/workos/v1/sessions/{session_id}/revoke(self-or-admin).src/WorkOS/Sessions/Controller.php— Registers Routes, admin assets, and webhook listeners (session.created,session.revoked). ExtendsWorkOS\Contracts\Controller.src/WorkOS/UI/SessionsBlock.php— Registers theworkos/user-sessionsblock; renders the Sessions React component.src/WorkOS/UI/SessionsShortcode.php— Registers[workos:user-sessions]shortcode; renders the Sessions React component.src/js/sessions/SessionsPanel.tsx(or.ts) — Single React component consumed by admin-side (mode="admin", current user id passed as prop) and front-end (mode="self", always current user). Calls REST endpoints, handles 401 redirect, displays session list with revoke buttons, pagination.Files to change
src/WorkOS/Api/Client.php— Add methodpublic function list_user_sessions( string $user_id, array $params = [] )callingGET /user_management/sessions?user_id=…. Pattern matches existing methods likelist_auth_factors()(lines 505-507) andlist_organization_memberships()(lines 701-703). Verify and match existing error handling and response envelope structure.src/WorkOS/Admin/UserProfile.php— Extend therender()method (line 69) to include a Sessions panel section below Recent Events (line 106). Fetch sessions using the Manager with caching (existing pattern at line 104). Render a table with sid, IP, user agent, created_at, last_active_at, revoke button. Call the endpoint for the edited user, not the current user.src/WorkOS/UI/Controller.php— Register SessionsBlock and SessionsShortcode indoRegister()(line 24) alongside existing Block and Ajax registrations. Enqueue Sessions assets (CSS, JS bundle) conditionally when the block or shortcode is present, matching the pattern for login-button (lines 59-112).src/WorkOS/ActivityLog/Controller.php— Add two new action hooks:add_action( 'workos_webhook_session.created', [ $this, 'log_session_created' ], 10, 1 )andadd_action( 'workos_webhook_session.revoked', [ $this, 'log_session_revoked' ], 10, 1 )indoRegister(). Implementlog_session_created()andlog_session_revoked()methods to callEventLogger::log()with appropriate metadata (session_id, user_id extracted from webhook, actor_id if revoked by admin).src/WorkOS/Admin/DiagnosticsPage.php— Add a Sessions API check: callManager::list_user_sessions()with the current user and report reachability or error. Integrate intorun_all_checks()(line 161).REST endpoints
GET /wp-json/workos/v1/sessions/me (requires auth, no user_id param)
{ "sessions": [ { "id", "ip_address", "user_agent", "created_at", "last_active_at", "is_current": bool } ], "pagination": { "after": "cursor_or_null" } }GET /wp-json/workos/v1/admin/users/{id}/sessions (requires manage_options)
{id}= WP user ID (routed param)./sessions/me.manage_optionscapability; admin may view any user's sessions.POST /wp-json/workos/v1/sessions/{session_id}/revoke (self-or-admin)
{session_id}= WorkOS session ID (routed param).{ "user_id": int }(the WP user owning the session; required for permission check).{ "ok": true }Client::revoke_session()(802); logs Activity event viaEventLogger::log( 'session_revoked', { 'user_id', 'session_id', 'actor_id', 'metadata': { 'was_current': bool } } ).Data model (options / user_meta / tables)
No new database schema. Uses existing
_workos_session_idmeta key (User.php:70). New activity log entries use the existingwp_workos_activity_logtable (EventLogger.php:51):event_type:'session_created'or'session_revoked'user_id: WP user ID (the user whose session was created/revoked).metadata: JSON object:{ "session_id": "...", "ip_address": "...", "user_agent": "...", "created_at": "...", "actor_id": int (only for revoked), "was_current": bool (only for revoked) }Session Manager uses transient cache:
workos_sessions_<md5(user_id)>Hooks & filters (new actions/filters this feature adds, named)
No new filters defined by this feature. The feature consumes existing actions:
workos_webhook_session.created— Fired by Webhook/Receiver.php when F3 expands the allowlist. Sessions/Controller listens.workos_webhook_session.revoked— Fired by Webhook/Receiver.php when F3 expands the allowlist. Sessions/Controller listens.Activity Log also listens to the catch-all
workos_webhook(WorkOS\Webhook\Receiver.php:134), already dispatched for all events post-F3.WorkOS API
GET /user_management/sessions
user_id(required),limit(default 25, max 100),after(cursor for pagination).{ "sessions": [ { "id": "sid_...", "ip_address": "...", "user_agent": "...", "created_at": "...", "last_active_at": "..." } ], "list_metadata": { "after": "cursor_or_null" } }POST /user_management/sessions/{id}/revoke
/user_management/sessions/{session_id}/revoke{ "id": "sid_...", "revoked_at": "...", ... }(Both endpoints already wrapped at Client.php:802 (revoke_session) and new list_user_sessions; no new wrapper needed beyond list_user_sessions.)
Edge cases & security
_workos_session_iduser meta to each session id; mark the match as "This device" in the UI.actor_id(the admin's WP user ID) separately fromuser_id(the target). EventLogger.log() receives metadata { "actor_id": get_current_user_id() } when called from the REST endpoint's revoke handler./admin/users/{id}/sessionsrequiresmanage_optionsso non-admins cannot enumerate sessions per user./sessions/meis limited to the authenticated user by REST/TokenAuth.php.permission_callbackis omitted or a custom auth is used). Bearer token requests skip nonce but are auth-protected by REST/TokenAuth.list_metadata.afteris present, show a "Load more" button.wp_logouthook in Sessions/Controller that callsManager::revoke_current_session()if desired (out of scope unless explicitly implemented).Testing
WPUnit (/slic)
Create the following test files under
tests/wpunit/:tests/wpunit/Sessions/ManagerTest.php — Tests the Manager class:
test_list_user_sessions_caches_60_seconds— Verify transient TTL.test_list_user_sessions_calls_api— Mock Client and verify the method calls it.test_list_user_sessions_returns_null_on_api_error— Verify error handling.test_revoke_session_clears_cache— Verify transient is cleared after revoke.tests/wpunit/Sessions/RestApiTest.php — Tests the REST endpoints:
test_get_sessions_me_requires_auth— 401 when not authenticated.test_get_sessions_me_returns_current_user_sessions— Authenticated request returns correct data.test_get_admin_sessions_requires_manage_options— 403 without capability.test_get_admin_sessions_returns_target_user_sessions— Admin can view any user's sessions.test_post_revoke_session_requires_self_or_admin— User cannot revoke others' sessions unless admin.test_post_revoke_session_calls_client— Verify Client::revoke_session() is called.test_post_revoke_session_logs_activity— Verify EventLogger::log() called with actor_id.tests/wpunit/Sessions/ControllerTest.php — Tests webhook listeners (assume F3 is merged first):
test_log_session_created_fires_on_webhook_event— Post asession.createdwebhook; verify Activity Log entry.test_log_session_revoked_fires_on_webhook_event— Post asession.revokedwebhook; verify Activity Log entry with actor_id.Browser smoke
In a Staging install (with WorkOS Staging org selected):
Admin Sessions panel:
_workos_session_id.End-user shortcode:
[workos:user-sessions].Webhook delivery (requires F3 merged):
session.createdevent via the WorkOS dashboard webhook tester.wp-admin/admin.php?page=workos-activity-log) → new entry with event_type = 'session_created'.session.revoked.Diagnostics & activity log
Diagnostics check (add to src/WorkOS/Admin/DiagnosticsPage.php run_all_checks, line 161):
Manager->list_user_sessions( get_current_user_id() )with caching disabled (or forced refresh).Activity Log event types:
session_created— Fired by the webhook listener onworkos_webhook_session.created. Metadata includes session_id, ip_address, user_agent, created_at.session_revoked— Fired by the webhook listener onworkos_webhook_session.revokedor by the REST revoke endpoint. Metadata includes session_id, actor_id (who revoked), was_current (bool).Documentation
docs/extending-the-login-ui.md(or createdocs/sessions.md):add_action( 'workos_webhook_session.created', function( $event ) { ... } );CHANGELOG.mdfor 1.1.0:[workos:user-sessions]shortcode and block."Out of scope
wp_logouthook integration (logout of WordPress when a WorkOS session is revoked) is out of scope but documented above for future inclusion.Dependencies & sequencing
Within 1.1.0, this feature depends on:
session.createdandsession.revoked. Without F3, webhook logging in this feature does not fire. Sequencing: F3 → F2 → F1.Sibling features in 1.1.0:
External dependencies:
GET /user_management/sessionsandPOST /user_management/sessions/{id}/revoke(already available in production).No breaking changes to public APIs — Sessions/Manager and Sessions/RestApi are new; existing User.php, Client.php, and ActivityLog/EventLogger are backward-compatible.
1.1.0 milestone navigation
Recommended landing order — #31 → #30 → #29; #28 is independent and may land any time. Each lands as soon as it is green (no monolithic 1.1.0 PR).
session.created/session.revokedeventsMilestone: https://github.com/bordoni/integration-workos/milestone/4