Skip to content

Feature: Admin Portal embed — SSO/Directory Sync/Audit Log self-serve #29

Description

@bordoni

Summary

Add a "Set up integrations" card to Settings → WorkOS → Organization with one button per Admin Portal intent (SSO, Directory Sync, Audit Logs, Log Streams, Domain Verification). Clicking a button calls a new admin REST endpoint that generates a temporary Admin Portal link from WorkOS and opens it in a new tab, allowing site admins to configure integrations without leaving WordPress.

Background / current state

Today, site admins must leave WordPress and visit the WorkOS dashboard to set up SSO, Directory Sync, Audit Log streaming, or domain verification. There is no in-WordPress interface for this configuration.

The plugin already has:

  • Organization management (src/WorkOS/Organization/Manager.php): allows selecting and syncing org metadata
  • Settings → WorkOS → Organization tab (src/WorkOS/Admin/Settings.php:755–785): renders org selector + entitlement gate option
  • API client (src/WorkOS/Api/Client.php): pattern for wrapping WorkOS endpoints (e.g., revoke_session at line 802)
  • REST API patterns (src/WorkOS/Admin/LoginProfiles/RestApi.php): admin-only REST routes with manage_options gate
  • Controller registration (src/WorkOS/Controller.php:37–56): hardcoded di52 container registration for feature controllers

What is missing:

  • A way for admins to click into Admin Portal from within WordPress
  • A new REST endpoint to generate the temporary link
  • A Client.php method wrapping POST /portal/generate_link
  • UI (card + buttons) on the Organization tab

Goals & acceptance criteria

  • Admin can navigate to Settings → WorkOS → Organization and see a "Set up integrations" card
  • Card displays five buttons: SSO, Directory Sync, Audit Logs, Log Streams, Domain Verification
  • Clicking any button calls POST /wp-json/workos/v1/admin/portal/link with the corresponding intent
  • Endpoint generates a link via WorkOS POST /portal/generate_link for the currently selected organization
  • Link opens in a new tab via window.open
  • If no organization is selected, all buttons are disabled with a tooltip "Select an organization first"
  • Network/API errors show a toast notification and do not leak the Admin Portal URL
  • Link generation respects the active environment's API key (Config::get_api_key()) so Staging and Production use their own endpoints
  • Diagnostics page has an "Admin Portal link generation" check that calls generate_admin_portal_link with domain_verification intent and reports pass/fail
  • Feature is fully tested via WPUnit under /slic
  • No breaking changes to the di52 controller architecture

Technical design

Files to add

  • src/WorkOS/AdminPortal/Controller.php — feature controller extending WorkOS\Contracts\Controller, registers REST + admin page assets in doRegister()
  • src/WorkOS/AdminPortal/RestApi.php — REST API class with register_routes() and callback for POST /wp-json/workos/v1/admin/portal/link
  • src/js/admin-portal/index.ts — TypeScript React component / vanilla JS to fetch the link endpoint and open it via window.open, with error handling
  • tests/wpunit/AdminPortal/RestApiTest.php — WPUnit tests for permission checks, intent validation, API error handling, and happy path
  • tests/wpunit/AdminPortal/DiagnosticsCheckTest.php — WPUnit test for the Diagnostics check callback

Files to change

  • src/WorkOS/Api/Client.php — add public method generate_admin_portal_link(string $org_id, string $intent, ?string $return_url = null): array|\WP_Error at line ~805 (after revoke_session), calling POST /portal/generate_link with { organization, intent, return_url }
  • src/WorkOS/Admin/Settings.php — extend register_organization_fields() (~755) to add a new settings section and rendered card with buttons (no new database option; card is static HTML/JS)
  • src/WorkOS/Controller.php — add $this->container->register( AdminPortal\Controller::class ); to the doRegister() list (~51)
  • src/WorkOS/Admin/DiagnosticsPage.php — extend run_all_checks() (~161) to add one check: call generate_admin_portal_link('domain_verification') and report pass/fail
  • webpack.config.js — extend or reuse existing admin bundle to include the new admin-portal JS

REST endpoints

POST /wp-json/workos/v1/admin/portal/link

  • Permission: manage_options required (checked in callback)
  • Parameters:
    • intent (string, required): one of sso, dsync, audit_logs, log_streams, domain_verification
    • organization_id (string, optional): if provided and user can verify they own it, use it; otherwise use Config::get_organization_id()
    • return_url (string, optional): callback URL after setup (defaults to WordPress home_url or omitted)
  • Response on success:
    • 200 OK with { url: "https://workos.com/..." }
  • Response on error:
    • 400 Bad Request if intent is invalid
    • 400 Bad Request if no organization is selected and organization_id not provided
    • 401 Unauthorized if user lacks manage_options
    • 500 Internal Server Error if WorkOS API fails (do NOT return the WorkOS error message verbatim; use generic message)

Data model (options / user_meta / tables)

No new database schema. The card is static HTML/JS rendered in the Organization tab via Settings::register_organization_fields(). No persistent state needed.

Hooks & filters

No new hooks or filters in this feature. The feature is UI-only and does not fire any custom actions or accept site-level customization. (Future: a workos_admin_portal_intents filter could allow custom intents; deferred to 1.2.0.)

WorkOS API

  • Endpoint: POST /portal/generate_link
  • Parameters: { organization: string, intent: string, return_url?: string }
  • Response: { url: string, expires_at: string } or error
  • Timeout: 5 minutes (WorkOS hardcoded; not configurable here)
  • Wrapped by: new Client.php method generate_admin_portal_link() (~805)

Edge cases & security

  • No organization selected: Button is disabled (via CSS class + aria-disabled) with a tooltip. The endpoint rejects requests without an org_id.
  • New install: If organization_id is empty, the card shows the buttons as disabled. No fallback to "Create Organization" (that flow is elsewhere in the Settings tab; users who haven't created an org yet are not the target of this card).
  • Network timeout / 4xx/5xx from WorkOS: Endpoint returns 500 with a generic error message like "Unable to generate link. Please try again." Client-side catches 500 and shows a toast. No Admin Portal URL is ever returned to the browser if the WorkOS call fails, preventing the leak of partial/invalid URLs.
  • Intent validation: RestApi::register_routes() validates intent against a hardcoded allowlist: ['sso', 'dsync', 'audit_logs', 'log_streams', 'domain_verification']. Any other intent returns 400 Bad Request.
  • CORS / nonce: The endpoint is a normal WordPress REST route (gated by manage_options). Standard WordPress REST nonce handling applies (no custom nonce needed for logged-in requests with permission).
  • Environment isolation: Config::get_api_key() and Config::get_organization_id() automatically select the active environment (Production or Staging), so admins using the Staging environment get Staging links and vice versa. No need to switch environments to test.
  • Rate limiting: Not implemented in this feature (none of the existing REST endpoints have per-user rate limiting; defer to future global rate-limit middleware if needed).

Testing

WPUnit (/slic)

All tests use the /slic harness (StellarWP Local Interactive Containers), not raw PHPUnit.

tests/wpunit/AdminPortal/RestApiTest.php

  • Test: generate_admin_portal_link() is called correctly on 200 response
  • Test: endpoint returns { url: "..." } on success
  • Test: intent validation rejects unknown intents with 400 Bad Request
  • Test: missing organization_id when Config::get_organization_id() is empty returns 400 Bad Request
  • Test: manage_options permission check: unauthenticated user gets 401, subscriber role gets 403, admin gets 200
  • Test: WorkOS API error (e.g., 401 Unauthorized from WorkOS) returns 500 with generic message, no WorkOS error message leaked
  • Test: request body parameters are sanitized (intent trimmed + lowercased, organization_id is string)

tests/wpunit/AdminPortal/DiagnosticsCheckTest.php

  • Test: Diagnostics::run_all_checks() includes a check named "Admin Portal link generation"
  • Test: check calls Client::generate_admin_portal_link with intent='domain_verification' and current org_id
  • Test: check passes if response is successful (has 'url' key)
  • Test: check fails and reports error message if WorkOS returns 4xx/5xx or network error

Browser smoke

In a Staging install (environment set to Staging, WorkOS Staging credentials configured):

  1. Log in as admin, go to Settings → WorkOS → Organization
  2. If no org is selected: confirm buttons are disabled and show "Select an organization first" on hover
  3. Select an organization from the dropdown and save
  4. Click the "SSO" button; new browser tab opens with a WorkOS Admin Portal link
  5. Repeat for one other button (e.g., "Directory Sync") to confirm intent parameter is passed correctly
  6. Go to Settings → WorkOS → Diagnostics, click "Run Diagnostics"
  7. Confirm "Admin Portal link generation" check passes and displays "Successfully generated Admin Portal link"

Diagnostics & activity log

Diagnostics check: "Admin Portal link generation"

  • Calls Client::generate_admin_portal_link($org_id, 'domain_verification', null) where $org_id = Config::get_organization_id()
  • Pass: "Successfully generated Admin Portal link"
  • Fail (if no org selected): "No organization configured"
  • Fail (if WorkOS error): Shows the WorkOS error message (e.g., "Unauthorized") or generic "Failed to generate link"

Activity log: No new event types. This feature does not log user actions (the Admin Portal link is used by the admin, not by end users). Once F3 (Webhook event coverage) widens the allowlist, admins can see SSO / Directory Sync activation events in the Activity Log once they've successfully set up those features via the Admin Portal.

Documentation

  • Update docs/extending-the-login-ui.md to add a section: "Admin Portal integration — how the Settings tab link generation works" with a note that this is an admin-only surface
  • No user-facing guide needed; the UI is self-explanatory

Out of scope

  • Custom intents: a workos_admin_portal_intents filter to allow plugins to register new intents (deferred to 1.2.0)
  • Embedding the Admin Portal iframe directly in WordPress (this is link generation only; full embed is a 2.0 feature)
  • End-user-facing Admin Portal (this is admin-only; user-facing will ship as a separate feature)
  • Rate limiting or abuse prevention beyond standard WordPress permissions (delegate to future global middleware)

Dependencies & sequencing

Within 1.1.0: This feature is independent of the other two (Sessions UI, Webhook event coverage). It can land in any order, but the recommended sequence is:

  1. F3 (Webhook event coverage) first — small change, unblocks observability for F2 + F1 validation
  2. F2 (Sessions UI) next — larger scope, establishes the admin REST + React patterns
  3. F1 (Admin Portal) last — smaller, uses patterns established by F2

This feature has no external dependencies on Sessions UI or Webhook coverage. It is purely additive and low-risk.

Cross-feature: Once F3 is merged, admins can verify that their Admin Portal setup worked by checking the Activity Log for connection.activated or dsync.activated events.


Implementation notes

  • The Settings.php Organization tab card should be rendered after the organization selector but before the Entitlement Gate section (keep org selection above config)
  • Ensure the card has a visual border / background to stand out from the Entitlement Gate checkbox
  • Use the existing workos-admin.css and admin bundle for styling; no separate CSS file needed unless the card is complex
  • The RestApi permission_callback should check current_user_can( 'manage_options' ) and return false (401) if not, matching the pattern in src/WorkOS/Admin/LoginProfiles/RestApi.php
  • Client.php generate_admin_portal_link() should follow the same $this->post(...) pattern as revoke_session() (line 802), returning array|\WP_Error

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 & screensenhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions