.do
Enterprises

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:

IntentPurposeFeatures
ssoConfigure SSO connectionIdP selection, SAML metadata, OAuth credentials, domain verification
dsyncConfigure Directory SyncProvider selection, SCIM endpoint, bearer token, attribute mapping
audit_logsConfigure Audit Log streamingStream destination, filters, retention
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:

sequenceDiagram participant Admin as IT Admin participant Portal as WorkOS Portal participant IdP as Identity Provider participant App as Your App Admin->>Portal: Open portal link Portal->>Admin: Show provider selection Admin->>Portal: Select Okta SAML Portal->>Admin: Show configuration steps Note over Admin,Portal: Step 1: IdP Information Admin->>Portal: Enter Okta metadata URL Note over Admin,Portal: Step 2: Verify Domains Admin->>Portal: Confirm acme.com domain Note over Admin,Portal: Step 3: Test Connection Portal->>IdP: Test SAML request IdP->>Portal: SAML response Portal->>Admin: ✓ Connection successful Note over Admin,Portal: Step 4: Activate Admin->>Portal: Activate SSO Portal->>App: Redirect to returnUrl

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
}

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 link

User Experience

Before Portal

Without the Admin Portal, SSO setup requires:

  1. Customer contacts support
  2. Support engineer creates ticket
  3. Customer provides IdP metadata via email/Slack
  4. Engineer manually configures connection
  5. Back-and-forth testing
  6. 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:

  1. Customer clicks "Configure SSO"
  2. Portal guides through setup (10-15 minutes)
  3. Customer tests connection themselves
  4. Customer activates when ready
  5. 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>
  )
}

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
`)