workos-sso
WorkOS Single Sign-On
Step 1: Fetch Documentation (BLOCKING)
STOP. Do not proceed until complete.
WebFetch these URLs in order — they are the source of truth for SSO implementation:
- https://workos.com/docs/sso/test-sso
- https://workos.com/docs/sso/single-logout
- https://workos.com/docs/sso/signing-certificates
- https://workos.com/docs/sso/sign-in-consent
- https://workos.com/docs/sso/saml-security
- https://workos.com/docs/sso/redirect-uris
- https://workos.com/docs/sso/login-flows
- https://workos.com/docs/sso/launch-checklist
If this skill conflicts with fetched docs, follow the docs.
Step 2: Pre-Flight Validation
Environment Variables
Check for required variables in .env.local or environment:
WORKOS_API_KEY— starts withsk_(production) orsk_test_(staging)WORKOS_CLIENT_ID— starts withclient_
Verify: Both keys exist and have correct prefixes. Staging keys access Test Identity Provider by default.
Redirect URI Configuration
Confirm callback URL is registered in WorkOS Dashboard:
- Log into https://dashboard.workos.com/
- Navigate to API Keys → Redirect URIs
- Verify your callback URL (e.g.,
https://your-app.com/sso/callback) is listed
Critical: Exact match required — trailing slashes and protocols must match.
Step 3: Install SDK
Detect package manager from lockfile, install WorkOS SDK:
# Detect which lockfile exists
ls package-lock.json yarn.lock pnpm-lock.yaml bun.lockb 2>/dev/null
# Install matching package manager
npm install @workos-inc/node
# OR
yarn add @workos-inc/node
# OR
pnpm add @workos-inc/node
# OR
bun add @workos-inc/node
Verify: SDK package exists in node_modules before continuing.
ls node_modules/@workos-inc/node/package.json
Step 4: Login Flow Selection (Decision Tree)
User initiates SSO from?
|
+-- Your app's login page
| --> Service Provider-Initiated (SP-initiated)
| --> User enters email → redirect to IdP → redirect back
| --> IMPLEMENTATION: Step 5
|
+-- Identity Provider portal
--> Identity Provider-Initiated (IdP-initiated)
--> User selects your app from IdP → redirect to your app
--> IMPLEMENTATION: Step 6 (callback only, no authorization URL)
Both flows require the same callback handler. Implement Step 5 for SP-initiated, then Step 6 for IdP-initiated support.
Step 5: Implement SP-Initiated SSO
5.1: Generate Authorization URL
Create endpoint to start SSO flow. Check fetched docs for exact method signature.
Typical pattern (verify against docs):
import { WorkOS } from '@workos-inc/node';
const workos = new WorkOS(process.env.WORKOS_API_KEY);
// Generate authorization URL
const authorizationUrl = workos.sso.getAuthorizationURL({
clientId: process.env.WORKOS_CLIENT_ID,
redirectUri: 'https://your-app.com/sso/callback',
// ONE OF:
organization: 'org_123', // For specific org
// OR
provider: 'GoogleOAuth', // For specific provider
// OR
domain: 'example.com', // For domain-based routing
});
// Redirect user to authorizationUrl
Decision tree for user identification:
How to identify which IdP?
|
+-- You know the organization ID
| --> Use organization parameter
|
+-- User enters email
| --> Extract domain → use domain parameter
| --> WorkOS routes to correct IdP
|
+-- Multi-provider login page
--> Use provider parameter (GoogleOAuth, MicrosoftOAuth, etc.)
Check fetched docs for supported provider values.
5.2: State Parameter (Optional but Recommended)
For CSRF protection and context preservation:
const state = generateRandomString(); // Your implementation
storeInSession(state); // Your session mechanism
const authorizationUrl = workos.sso.getAuthorizationURL({
// ... other params
state: state,
});
Verify state in callback handler (Step 6.2).
Step 6: Implement Callback Handler
6.1: Create Callback Route
Create handler at your configured redirect URI path (e.g., /sso/callback).
Check fetched docs for authenticateWithCode method signature:
// Callback handler receives: code, state parameters
const { code, state, error, error_description } = request.query;
// Handle errors first (Step 6.3)
if (error) {
// See Step 6.3
}
// Exchange code for profile
const profile = await workos.sso.authenticateWithCode({
clientId: process.env.WORKOS_CLIENT_ID,
code: code,
});
// profile contains:
// - profile.id (unique user ID)
// - profile.email
// - profile.firstName, profile.lastName
// - profile.organizationId
// - profile.connectionId
6.2: State Validation (If Used)
const storedState = getFromSession(); // Your session mechanism
if (state !== storedState) {
throw new Error('State mismatch — possible CSRF attack');
}
6.3: Error Handling (CRITICAL)
Required error codes to handle:
switch (error) {
case 'signin_consent_denied':
// User denied consent at IdP
// Action: Show "Contact your admin" message
// Do NOT retry automatically — user explicitly declined
break;
case 'invalid_request':
// Configuration issue (wrong redirect URI, client ID, etc.)
// Action: Log error, show generic "Configuration error"
// Fix: Check Dashboard settings
break;
case 'access_denied':
// IdP rejected authentication
// Action: Show "Access denied by your organization"
break;
default:
// Generic error
// error_description contains details
// Log for debugging, show generic error to user
}
Check fetched docs for complete error code list.
6.4: Session Management
After successful authentication:
// Create session with profile data
const session = {
userId: profile.id,
email: profile.email,
organizationId: profile.organizationId,
};
// Store session (your implementation)
setUserSession(session);
// Redirect to app dashboard
redirect('/dashboard');
Critical for IdP-initiated: Callback handler must work WITHOUT prior authorization URL generation. User arrives directly from IdP with code.
Step 7: Test with Test Identity Provider
7.1: Verify Test Organization Exists
Navigate to Dashboard staging environment:
- Log into https://dashboard.workos.com/
- Switch to staging environment (top right)
- Navigate to Organizations
- Confirm "Test Organization" exists with active SSO connection
If missing: Your staging environment should have this by default. Contact WorkOS support.
7.2: Test SP-Initiated Flow
# Start your app
npm run dev
# Navigate to login page
open http://localhost:3000/login
# Enter test email: test@example.com
# Should redirect to Test IdP
# Click "Sign in" on Test IdP
# Should redirect back to your app with successful authentication
Verify: Check callback handler receives code parameter, not error.
7.3: Test IdP-Initiated Flow (CRITICAL)
Important: Disable AuthKit first if enabled:
- Navigate to https://dashboard.workos.com/authentication
- Toggle AuthKit OFF
- Save
Then test:
- Navigate to Test SSO page: https://dashboard.workos.com/test-sso
- Click "Test IdP-initiated SSO" link
- Should land directly on Test IdP
- Click "Sign in"
- Should redirect to your callback handler
If fails: Check callback handler doesn't require prior authorization URL generation.
7.4: Test Error Scenarios
From Test SSO page, test:
- Consent Denied: Click "Deny" on Test IdP → verify
signin_consent_deniederror handled - Guest Email: Use
guest@different-domain.com→ verify profile contains guest email - Generic Error: Click "Simulate Error" → verify
erroranderror_descriptionhandled
Step 8: Configure Production Organization
8.1: Create Organization
In Dashboard production environment:
- Navigate to Organizations
- Click "Create organization"
- Enter customer name (e.g., "Acme Corp")
- Enter domain (e.g., "acme.com")
- Save
8.2: Enable SSO Connection
Decision tree for setup method:
Who configures SSO?
|
+-- Your team (white-glove setup)
| --> Manual connection creation (Step 8.3)
|
+-- Customer admin (self-serve)
--> Admin Portal invitation (Step 8.4)
8.3: Manual Connection Setup (White-Glove)
- Open organization in Dashboard
- Click "Create connection"
- Select identity provider (Okta, Azure AD, Google, etc.)
- Follow provider-specific instructions from fetched docs
- Test connection with "Test Connection" button
Check workos-integrations skill for provider-specific setup details.
8.4: Admin Portal Invitation (Self-Serve)
- Open organization in Dashboard
- Click "Invite admin"
- Select "Single Sign-On"
- Enter customer admin email OR copy setup link
- Customer follows Admin Portal instructions
Verify: Connection status changes to "Active" after customer completes setup.
Step 9: Single Logout (Optional)
Check fetched docs for current support status. As of last update:
- Supported for OpenID Connect connections only
- Not supported for SAML connections
- Limited scenarios
Implementation (if supported)
// RP-initiated logout
const logoutUrl = workos.sso.getLogoutURL({
sessionId: profile.sessionId, // From authentication response
});
// Redirect user to logoutUrl
// This logs out from your app AND IdP
Fallback: If Single Logout not supported, implement local logout only:
// Clear local session
clearUserSession();
redirect('/login');
// User remains logged into IdP (standard behavior)
Verification Checklist (ALL MUST PASS)
Run these commands to verify integration:
# 1. Environment variables exist
grep -E "WORKOS_API_KEY|WORKOS_CLIENT_ID" .env.local || echo "FAIL: Missing env vars"
# 2. SDK installed
ls node_modules/@workos-inc/node/package.json || echo "FAIL: SDK not installed"
# 3. Callback route exists (adjust path to your implementation)
find . -name "*callback*" -o -name "*sso*" | grep -v node_modules
# 4. Test authentication with curl (requires running app)
curl -I http://localhost:3000/sso/callback?code=test 2>/dev/null | grep -E "200|302" || echo "FAIL: Callback not responding"
# 5. Production build succeeds
npm run build
Manual verification:
- SP-initiated flow works with Test IdP
- IdP-initiated flow works with Test IdP
-
signin_consent_deniederror displays helpful message - Callback handler logs profile data correctly
- Session persists across page reloads
- Production organization has active SSO connection
Error Recovery
"Connection not found"
Root cause: Authorization URL uses organization parameter, but organization has no active SSO connection.
Fix:
- Check organization in Dashboard
- Verify SSO connection status is "Active"
- For test: Use "Test Organization" in staging environment
- For production: Customer must complete Admin Portal setup
"Redirect URI mismatch"
Root cause: Callback URL doesn't match Dashboard configuration EXACTLY.
Fix:
- Check actual callback URL in browser (after IdP redirect)
- Log into Dashboard → API Keys → Redirect URIs
- Add exact URL including protocol and trailing slash
- Common mismatch:
http://localhost:3000/callback/vshttp://localhost:3000/callback
"Invalid client credentials"
Root cause: Wrong WORKOS_CLIENT_ID or WORKOS_API_KEY.
Fix:
- Verify keys in Dashboard → API Keys
- Check environment (staging keys work only in staging)
- Confirm no extra whitespace in
.env.local - Restart app after changing env vars
"State mismatch" or "Invalid state"
Root cause: State parameter implementation broken or missing.
Fix:
- If not using state: Remove state validation from callback handler
- If using state: Check session storage mechanism works
- Verify state generation uses cryptographically secure random
- Check session doesn't expire between authorization URL and callback
Callback receives error=invalid_request
Root cause: Configuration error in authorization URL generation.
Fix:
- Check parameters passed to
getAuthorizationURL:clientIdmatches DashboardredirectUrimatches Dashboard exactly- At least one of:
organization,provider, ordomain
- Check SDK version supports your parameter combination
- Review fetched docs for required parameters
User sees "This app hasn't been verified" (Google OAuth)
Root cause: Google SSO connection not verified with Google.
Fix:
- This is expected for development/staging
- For production: Follow Google OAuth verification process
- Alternatively: Use Test IdP or different provider for testing
- Users can click "Advanced" → "Go to [app] (unsafe)" to proceed (dev only)
IdP-initiated flow lands on 404
Root cause: Callback handler doesn't handle missing state parameter.
Fix:
- Make
stateparameter optional in callback handler - IdP-initiated flow doesn't include state (by design)
- Check:
const state = request.query.state || null;
"cookies was called outside request scope" (Next.js 15+)
Root cause: Async cookie operations not properly awaited.
Fix:
- Ensure callback handler is async
- Await all session operations
- Check session library compatibility with Next.js 15+
- See
workos-authkit-nextjsskill for Next.js-specific patterns
Related Skills
- workos-authkit-nextjs: Higher-level Next.js SSO integration with UI components
- workos-integrations: Provider-specific SSO setup instructions (Okta, Azure AD, Google, etc.)
- workos-admin-portal: Self-serve SSO setup for customers
- workos-directory-sync: Sync user directories from IdPs after SSO is configured
- workos-rbac: Add role-based access control after SSO authentication
- workos-domain-verification: Verify domain ownership before enabling SSO
More from workos/skills
workos
Use when the user asks for a WorkOS docs URL, term, or dashboard field (Sign-in endpoint, initiate_login_uri, Redirect URI, `WORKOS_*` env vars), or is implementing, debugging, or migrating WorkOS — AuthKit, SSO/SAML, Directory Sync, RBAC, FGA, MFA, Vault, Audit Logs, Admin Portal, Pipes (Connected Apps), Feature Flags, Radar (bot/fraud detection), webhooks, Custom Domains, or migrating from Auth0, Clerk, Cognito, Firebase, Supabase, Stytch, Descope, or Better Auth. Also triggers on @workos-inc/* imports.
595workos-widgets
Use when the user is implementing, embedding, or debugging a WorkOS Widget — specifically the User Management, User Profile, Admin Portal SSO Connection, or Admin Portal Domain Verification widgets. Handles the full stack — detecting the frontend (Next.js, React, React Router, TanStack Start, Vite, SvelteKit), generating access tokens via the backend SDK in use (Node, Python, Go, Ruby, PHP, Java, .NET), and wiring up the widget component correctly per the bundled OpenAPI spec. Also use when code imports from @workos-inc/widgets or the user pastes <UserManagement /> or <UserProfile /> JSX.
270workos-authkit-nextjs
Integrate WorkOS AuthKit with Next.js App Router (13+). Server-side rendering required.
64workos-authkit-base
Architectural reference for WorkOS AuthKit integrations. Fetch README first for implementation details.
42workos-authkit-react
Integrate WorkOS AuthKit with React single-page applications. Client-side only authentication. Use when the project is a React SPA without Next.js or React Router.
33workos-rbac
Set up role-based access control for your application.
4