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
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):
- Log in as admin, go to Settings → WorkOS → Organization
- If no org is selected: confirm buttons are disabled and show "Select an organization first" on hover
- Select an organization from the dropdown and save
- Click the "SSO" button; new browser tab opens with a WorkOS Admin Portal link
- Repeat for one other button (e.g., "Directory Sync") to confirm intent parameter is passed correctly
- Go to Settings → WorkOS → Diagnostics, click "Run Diagnostics"
- 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:
- F3 (Webhook event coverage) first — small change, unblocks observability for F2 + F1 validation
- F2 (Sessions UI) next — larger scope, establishes the admin REST + React patterns
- 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
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:
What is missing:
Goals & acceptance criteria
Technical design
Files to add
Files to change
generate_admin_portal_link(string $org_id, string $intent, ?string $return_url = null): array|\WP_Errorat line ~805 (after revoke_session), calling POST /portal/generate_link with { organization, intent, return_url }$this->container->register( AdminPortal\Controller::class );to the doRegister() list (~51)REST endpoints
POST /wp-json/workos/v1/admin/portal/link
intent(string, required): one ofsso,dsync,audit_logs,log_streams,domain_verificationorganization_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){ url: "https://workos.com/..." }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_intentsfilter could allow custom intents; deferred to 1.2.0.)WorkOS API
{ organization: string, intent: string, return_url?: string }{ url: string, expires_at: string }or errorgenerate_admin_portal_link()(~805)Edge cases & security
['sso', 'dsync', 'audit_logs', 'log_streams', 'domain_verification']. Any other intent returns 400 Bad Request.Testing
WPUnit (/slic)
All tests use the /slic harness (StellarWP Local Interactive Containers), not raw PHPUnit.
tests/wpunit/AdminPortal/RestApiTest.php
tests/wpunit/AdminPortal/DiagnosticsCheckTest.php
Browser smoke
In a Staging install (environment set to Staging, WorkOS Staging credentials configured):
Diagnostics & activity log
Diagnostics check: "Admin Portal link generation"
Client::generate_admin_portal_link($org_id, 'domain_verification', null)where$org_id = Config::get_organization_id()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
Out of scope
workos_admin_portal_intentsfilter to allow plugins to register new intents (deferred to 1.2.0)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:
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.activatedordsync.activatedevents.Implementation notes
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