Skip to content

Feature: Sessions UI — list & revoke active WorkOS sessions #30

Description

@bordoni

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_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).
  • REST endpoints (/sessions/me, /admin/users/{id}/sessions, /sessions/{session_id}/revoke) validate permissions correctly.
  • Diagnostics page includes a Sessions API connectivity check.
  • WPUnit tests cover Manager caching, REST permission checks, and webhook logging.

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). Extends WorkOS\Contracts\Controller.
  • 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).
  • Returns: { "sessions": [ { "id", "ip_address", "user_agent", "created_at", "last_active_at", "is_current": bool } ], "pagination": { "after": "cursor_or_null" } }
  • Permissions: Authenticated user (cookie or Bearer token via REST/TokenAuth.php).
  • Caching: Manager caches 60 seconds per user_id.
  • Error: 401 if not authenticated.

GET /wp-json/workos/v1/admin/users/{id}/sessions (requires manage_options)

  • Purpose: List sessions for a target user (admin feature).
  • Parameters: {id} = WP user ID (routed param).
  • Returns: Same structure as /sessions/me.
  • Permissions: manage_options capability; admin may view any user's sessions.
  • Caching: Manager caches 60 seconds per user_id.
  • Error: 403 if not manage_options; 404 if user not found.

POST /wp-json/workos/v1/sessions/{session_id}/revoke (self-or-admin)

  • Purpose: Revoke a session by ID.
  • Parameters: {session_id} = WorkOS session ID (routed param).
  • Body: { "user_id": int } (the WP user owning the session; required for permission check).
  • Returns: { "ok": true }
  • Permissions: User may revoke their own sessions; admin (manage_options) may revoke any user's session.
  • Behavior: Calls Client::revoke_session() (802); logs Activity event via EventLogger::log( 'session_revoked', { 'user_id', 'session_id', 'actor_id', 'metadata': { 'was_current': bool } } ).
  • 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).
  • Response: { "sessions": [ { "id": "sid_...", "ip_address": "...", "user_agent": "...", "created_at": "...", "last_active_at": "..." } ], "list_metadata": { "after": "cursor_or_null" } }
  • Errors: 400 if user_id invalid; 401 if API key invalid.

POST /user_management/sessions/{id}/revoke

  • Path: /user_management/sessions/{session_id}/revoke
  • Body: empty
  • Response: { "id": "sid_...", "revoked_at": "...", ... }
  • 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:

    • 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 a session.created webhook; verify Activity Log entry.
    • test_log_session_revoked_fires_on_webhook_event — Post a session.revoked webhook; verify Activity Log entry with actor_id.

Browser smoke

In a Staging install (with WorkOS Staging org selected):

  1. Admin Sessions panel:

    • Log in to WordPress as admin.
    • Navigate to Users → edit any user.
    • Scroll to "WorkOS Sessions" section.
    • Verify: table shows at least one session (the current login).
    • Verify: each row shows sid, IP, user agent, created_at, last_active_at, Revoke button.
    • Verify: "This device" label appears on the session matching _workos_session_id.
    • Click Revoke on a non-current session → row disappears, Activity Log entry appears.
  2. End-user shortcode:

    • Create a page with [workos:user-sessions].
    • Log in to the site as a non-admin user.
    • View the page.
    • Verify: shortcode renders a session list.
    • Verify: user can only see their own sessions (no admin drop-down).
    • Open a second browser, log in as the same user.
    • On the first browser, refresh the session list; verify both sessions appear.
    • Click Revoke on the second browser's session; refresh the first.
    • Verify: second session is gone.
    • Try to use the second browser → should 401 or show login prompt.
  3. Webhook delivery (requires F3 merged):

    • Set up a Staging WorkOS org with webhooks enabled.
    • Trigger a session.created event via the WorkOS dashboard webhook tester.
    • Check Activity Log (wp-admin/admin.php?page=workos-activity-log) → new entry with event_type = 'session_created'.
    • Repeat for session.revoked.

Diagnostics & activity log

Diagnostics check (add to src/WorkOS/Admin/DiagnosticsPage.php run_all_checks, line 161):

  • Name: "Sessions API connectivity"
  • Test: Call Manager->list_user_sessions( get_current_user_id() ) with caching disabled (or forced refresh).
  • 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).

Documentation

  • Update docs/extending-the-login-ui.md (or create docs/sessions.md):
    • 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).

Milestone: https://github.com/bordoni/integration-workos/milestone/4

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: adminWP-admin settings & screensarea: authAuthentication / AuthKit / Login ProfilesenhancementNew feature or requestsecuritySecurity-relevant change

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions