Admin Portal
Self-service WorkOS Admin Portal for enterprise IT administrators to configure SSO, Directory Sync, and manage integrations without developer involvement
Admin Portal
The WorkOS Admin Portal is a self-service interface that enables your enterprise customers' IT administrators to configure SSO, Directory Sync, and other enterprise features without requiring developer or support involvement.
Overview
The Admin Portal provides:
- Self-Service Configuration - IT admins can configure integrations independently
- No Developer Required - Zero-touch setup for standard configurations
- Guided Workflows - Step-by-step instructions for each provider
- Real-Time Testing - Test connections before going live
- Audit Trail - Track all configuration changes
- Multi-Provider Support - Support for 50+ identity providers
Portal Intents
The Admin Portal supports different configuration workflows called intents:
| Intent | Purpose | Features |
|---|---|---|
sso | Configure SSO connection | IdP selection, SAML metadata, OAuth credentials, domain verification |
dsync | Configure Directory Sync | Provider selection, SCIM endpoint, bearer token, attribute mapping |
audit_logs | Configure Audit Log streaming | Stream destination, filters, retention |
Generating Portal Links
Basic Portal Link
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY)
// Generate SSO configuration portal link
const { link } = await workos.portal.generateLink({
organization: 'org_01HXYZ...',
intent: 'sso',
returnUrl: 'https://platform.do/settings/sso'
})
console.log(link)
// https://id.workos.com/portal/launch?secret=secret_01ABC...
// Send link to IT admin
await send($.Email.send, {
to: '[email protected]',
subject: 'Configure SSO for your organization',
template: 'admin-portal-invitation',
data: {
portalLink: link,
organizationName: 'Acme Corporation'
}
})Directory Sync Portal
// Generate Directory Sync configuration portal
const { link } = await workos.portal.generateLink({
organization: 'org_01HXYZ...',
intent: 'dsync',
returnUrl: 'https://platform.do/settings/directory'
})Success Callback URL
Specify where to redirect after successful configuration:
const { link } = await workos.portal.generateLink({
organization: 'org_01HXYZ...',
intent: 'sso',
returnUrl: 'https://platform.do/settings/sso?configured=true',
successUrl: 'https://platform.do/onboarding/next-step'
})Portal Workflows
SSO Configuration Workflow
When an IT admin opens an SSO portal link:
Configuration Steps
1. Provider Selection
Admin selects from 50+ providers:
- Okta
- Microsoft Entra (Azure AD)
- Google Workspace
- OneLogin
- Auth0
- Generic SAML 2.0
- And more...
2. Connection Details
Depending on the provider:
SAML 2.0:
- SAML Metadata URL or XML
- Entity ID
- SSO URL
- X.509 Certificate
OAuth 2.0:
- Client ID
- Client Secret
- Authorization URL
- Token URL
3. Domain Verification
Verify ownership of company domains:
- acme.com
- acme.co.uk
- etc.
4. Test Connection
Test the configuration before activating:
- Initiate test login
- Complete authentication with IdP
- Verify user profile data returned
5. Activation
Activate SSO for the organization.
Embedding the Portal
In-App Iframe
Embed the portal directly in your application:
import { useEffect, useState } from 'react'
export function SSOConfigurationPage({ organization }) {
const [portalLink, setPortalLink] = useState(null)
useEffect(() => {
async function generateLink() {
const response = await fetch('/api/admin-portal/sso', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ organizationId: organization.id })
})
const { link } = await response.json()
setPortalLink(link)
}
generateLink()
}, [organization.id])
if (!portalLink) return <div>Loading...</div>
return (
<div className="admin-portal-container">
<h1>Configure Single Sign-On</h1>
<iframe
src={portalLink}
width="100%"
height="800"
frameBorder="0"
allow="clipboard-write"
/>
</div>
)
}API Endpoint
// API endpoint to generate portal links
on($.API.request, async ({ path, user, body }) => {
if (path === '/api/admin-portal/sso') {
// Verify user has admin role
if (!hasPermission(user, 'sso:configure')) {
throw new Error('Insufficient permissions')
}
const { link } = await workos.portal.generateLink({
organization: user.organizationId,
intent: 'sso',
returnUrl: `${process.env.APP_URL}/settings/sso`
})
return { link }
}
})Customization
Branding
Customize portal appearance (Enterprise plan):
// Configure portal branding via WorkOS Dashboard
// - Logo
// - Primary color
// - Company name
// - Custom domain (portal.acme.com)Email Templates
Customize invitation emails:
// Email template for portal invitation
const template = `
Hi {{admin_name}},
Your organization ({{organization_name}}) is ready to configure Single Sign-On!
This will allow your team members to sign in using your company's identity provider
(Okta, Microsoft Entra, Google Workspace, etc.).
Configure SSO now: {{portal_link}}
This link expires in 24 hours.
Questions? Reply to this email or contact [email protected]
Best regards,
The {{app_name}} Team
`Portal Events
Monitor portal activity via webhooks:
on($.Webhook.received, async ({ event }) => {
switch (event.event) {
case 'connection.activated':
// SSO connection activated
await handleConnectionActivated(event.data)
break
case 'connection.deactivated':
// SSO connection deactivated
await handleConnectionDeactivated(event.data)
break
case 'dsync.activated':
// Directory Sync activated
await handleDirectorySyncActivated(event.data)
break
case 'dsync.deleted':
// Directory Sync deleted
await handleDirectorySyncDeleted(event.data)
break
}
})
async function handleConnectionActivated({ id, organization_id, type }) {
// Log SSO activation
await send($.Event.log, {
type: 'sso.activated',
organizationId: organization_id,
metadata: {
connectionId: id,
connectionType: type
}
})
// Notify organization admins
const admins = await db.list($.User, {
organizationId: organization_id,
role: 'admin'
})
for (const admin of admins) {
await send($.Email.send, {
to: admin.email,
template: 'sso-activated',
data: {
connectionType: type,
organization: await db.find($.Organization, organization_id)
}
})
}
// Update organization settings
await $.Organization.update(organization_id, {
ssoEnabled: true,
ssoType: type
})
}Access Control
Portal Access Management
Control who can access the portal:
// Only allow organization admins to generate portal links
async function generatePortalLink(userId, intent) {
const user = await db.find($.User, userId)
// Check if user is admin
if (user.role !== 'admin' && user.role !== 'owner') {
throw new Error('Only organization administrators can access the Admin Portal')
}
// Check specific intent permissions
const requiredPermission = {
sso: 'sso:configure',
dsync: 'directory:configure',
audit_logs: 'audit:configure'
}[intent]
if (!hasPermission(user, requiredPermission)) {
throw new Error(`Permission denied: ${requiredPermission} required`)
}
// Generate portal link
const { link } = await workos.portal.generateLink({
organization: user.organizationId,
intent,
returnUrl: `${process.env.APP_URL}/settings/${intent}`
})
// Log portal access
await send($.Event.log, {
type: 'admin_portal.accessed',
userId: user.id,
organizationId: user.organizationId,
metadata: { intent }
})
return link
}Link Expiration
Portal links expire after 24 hours by default:
// Links are single-use and expire after 24 hours
const { link } = await workos.portal.generateLink({
organization: 'org_01HXYZ...',
intent: 'sso',
returnUrl: 'https://platform.do/settings/sso'
})
// Link is valid for 24 hours
// After expiration, generate a new linkUser Experience
Before Portal
Without the Admin Portal, SSO setup requires:
- Customer contacts support
- Support engineer creates ticket
- Customer provides IdP metadata via email/Slack
- Engineer manually configures connection
- Back-and-forth testing
- Days or weeks to complete
Problems:
- High support overhead
- Slow time-to-value
- Security risk (credentials shared via email)
- Manual work doesn't scale
After Portal
With the Admin Portal:
- Customer clicks "Configure SSO"
- Portal guides through setup (10-15 minutes)
- Customer tests connection themselves
- Customer activates when ready
- Setup complete
Benefits:
- Zero support tickets
- Instant activation
- Secure (no credential sharing)
- Scales infinitely
Best Practices
1. Contextual Help
Provide guidance within your app:
function SSOSettingsPage({ organization }) {
return (
<div>
<h1>Single Sign-On</h1>
{!organization.ssoEnabled ? (
<div className="setup-prompt">
<h2>Enable SSO for your team</h2>
<p>
Allow your employees to sign in using your company's identity provider
(Okta, Microsoft Entra, Google Workspace, etc.)
</p>
<ul>
<li>✓ Centralized user management</li>
<li>✓ Enforce MFA and security policies</li>
<li>✓ Automatic onboarding/offboarding</li>
<li>✓ SOC 2 & compliance requirements</li>
</ul>
<button onClick={() => openAdminPortal('sso')}>
Configure SSO →
</button>
<details>
<summary>How does SSO setup work?</summary>
<ol>
<li>Select your identity provider (Okta, Entra, etc.)</li>
<li>Enter your IdP metadata</li>
<li>Verify your company domain</li>
<li>Test the connection</li>
<li>Activate SSO for your team</li>
</ol>
<p>Setup takes 10-15 minutes. No developer required.</p>
</details>
</div>
) : (
<div className="sso-active">
<h2>SSO is active</h2>
<p>Type: {organization.ssoType}</p>
<button onClick={() => openAdminPortal('sso')}>
Manage SSO Settings
</button>
</div>
)}
</div>
)
}2. Progressive Onboarding
Introduce enterprise features at the right time:
// Show SSO setup after organization reaches 10 users
on($.User.created, async ({ organizationId }) => {
const userCount = await db.count($.User, { organizationId })
const org = await db.find($.Organization, organizationId)
if (userCount === 10 && !org.ssoEnabled && !org.ssoPromptDismissed) {
// Trigger in-app notification
await send($.Notification.create, {
userId: org.ownerId,
type: 'sso_recommendation',
title: 'Consider enabling SSO',
message: 'Your team has grown to 10 users. SSO can streamline authentication and improve security.',
actions: [
{ label: 'Configure SSO', action: 'open_portal:sso' },
{ label: 'Learn More', action: 'open_docs:sso' },
{ label: 'Dismiss', action: 'dismiss' }
]
})
}
})3. Status Monitoring
Show real-time configuration status:
function SSOStatusIndicator({ organization }) {
const [connection, setConnection] = useState(null)
useEffect(() => {
async function fetchStatus() {
const response = await fetch(`/api/sso/status/${organization.id}`)
const data = await response.json()
setConnection(data)
}
fetchStatus()
const interval = setInterval(fetchStatus, 30000) // Poll every 30s
return () => clearInterval(interval)
}, [organization.id])
if (!connection) return null
return (
<div className={`status-indicator status-${connection.state}`}>
{connection.state === 'active' && (
<>
<span className="icon">✓</span>
<span>SSO Active ({connection.type})</span>
</>
)}
{connection.state === 'inactive' && (
<>
<span className="icon">○</span>
<span>SSO Not Configured</span>
</>
)}
{connection.state === 'error' && (
<>
<span className="icon">!</span>
<span>SSO Configuration Error</span>
<button onClick={() => openAdminPortal('sso')}>
Fix Issue
</button>
</>
)}
</div>
)
}4. Documentation Links
Link to provider-specific guides:
const providerGuides = {
'OktaSAML': 'https://docs.yourapp.do/sso/okta',
'AzureSAML': 'https://docs.yourapp.do/sso/azure',
'GoogleSAML': 'https://docs.yourapp.do/sso/google',
'GenericSAML': 'https://docs.yourapp.do/sso/generic-saml'
}
// Show relevant guide based on provider
function SSOHelp({ connection }) {
const guideUrl = providerGuides[connection?.type]
return guideUrl ? (
<a href={guideUrl} target="_blank">
View {connection.type} setup guide →
</a>
) : null
}Troubleshooting
Common Issues
Portal link not working
- Links expire after 24 hours
- Generate a new link
- Check browser console for errors
"Organization not found"
- Verify organization ID is correct
- Ensure organization exists in WorkOS
Iframe blocked by browser
- Some browsers block third-party cookies
- Use direct link instead of iframe
- Or configure custom domain for portal
Connection test fails
- Verify IdP metadata is correct
- Check SAML certificate is valid
- Ensure domains are verified
- Review WorkOS event logs
Debug Mode
Enable detailed logging:
// Log all portal link generations
on($.AdminPortal.link_generated, async ({ userId, organizationId, intent, link }) => {
await send($.Event.log, {
type: 'admin_portal.link_generated',
userId,
organizationId,
metadata: {
intent,
linkId: extractLinkId(link),
expiresAt: addHours(new Date(), 24)
}
})
})
// Track portal completions
on($.Webhook.received, async ({ event }) => {
if (event.event === 'connection.activated') {
await send($.Event.log, {
type: 'admin_portal.sso_configured',
organizationId: event.data.organization_id,
metadata: {
connectionType: event.data.type,
configuredViaPortal: true
}
})
}
})Analytics
Track portal adoption and completion rates:
// Portal funnel metrics
const metrics = await db.query(`
SELECT
COUNT(DISTINCT organization_id) FILTER (WHERE event_type = 'admin_portal.link_generated') as portals_opened,
COUNT(DISTINCT organization_id) FILTER (WHERE event_type = 'admin_portal.sso_configured') as sso_completed,
COUNT(DISTINCT organization_id) FILTER (WHERE event_type = 'admin_portal.dsync_configured') as dsync_completed,
ROUND(
COUNT(DISTINCT organization_id) FILTER (WHERE event_type = 'admin_portal.sso_configured')::numeric /
NULLIF(COUNT(DISTINCT organization_id) FILTER (WHERE event_type = 'admin_portal.link_generated'), 0) * 100,
2
) as completion_rate
FROM events
WHERE timestamp >= NOW() - INTERVAL '30 days'
`)
// Average time to completion
const avgTime = await db.query(`
SELECT
AVG(completed_at - started_at) as avg_completion_time
FROM (
SELECT
organization_id,
MIN(timestamp) FILTER (WHERE event_type = 'admin_portal.link_generated') as started_at,
MIN(timestamp) FILTER (WHERE event_type = 'admin_portal.sso_configured') as completed_at
FROM events
WHERE timestamp >= NOW() - INTERVAL '30 days'
GROUP BY organization_id
) t
WHERE completed_at IS NOT NULL
`)Related Documentation
- Enterprise SSO - Single Sign-On authentication
- Directory Sync - Automatic user provisioning
- Organizations - Multi-tenant organization management
- Audit Logs - Compliance and security monitoring
- WorkOS Admin Portal Docs - Official WorkOS documentation
Organizations
Manage multi-tenant enterprise organizations with WorkOS, including domain verification, member management, and hierarchical structures
Audit Logs & Compliance
Enterprise-grade audit logging with WorkOS for compliance, security monitoring, and incident response supporting SOC 2, HIPAA, and GDPR requirements