WorkOS: Add Enterprise SSO to Your SaaS in Hours
Imagine a SaaS product that instantly speaks the language of Fortune 500 security teams—no custom SAML wiring, no endless back‑and‑forth with IT admins. With WorkOS, you can embed enterprise‑grade Single Sign‑On (SSO) in just a few hours, not weeks. In this guide we’ll walk through the core concepts, walk you through a live integration, and sprinkle in pro tips you won’t find in the official docs.
Why SSO Matters for Modern SaaS
Enterprises demand a single source of truth for authentication. By delegating login to an identity provider (IdP) such as Okta, Azure AD, or OneLogin, they reduce password fatigue, enforce MFA, and stay compliant with regulations like SOC 2 and GDPR.
From a product perspective, SSO is a competitive moat. A smooth, secure login experience can shave days off a sales cycle and lower churn because users stay within the familiar corporate login flow.
Key Benefits
- Security: Credentials never touch your servers; the IdP handles authentication.
- Compliance: Leverage the IdP’s audit logs and MFA policies.
- Productivity: Users log in once and get seamless access to all SaaS tools.
- Scalability: Adding a new customer is a matter of configuration, not code.
What WorkOS Brings to the Table
WorkOS abstracts the messy details of SAML, OIDC, and SCIM behind a unified API. It provides:
- Pre‑built IdP connectors for 30+ providers.
- Hosted login pages that can be white‑labeled.
- Directory sync (SCIM) for provisioning users automatically.
- Audit logs and risk insights out of the box.
The magic lies in its developer‑first approach: you only need a few environment variables and a couple of endpoint handlers to get up and running.
Pro tip: Enable WorkOS’s “auto‑provisioning” feature during the beta phase. It saves you from writing custom SCIM endpoints later.
Getting Started: Prerequisites
Before you write a single line of code, make sure you have:
- A WorkOS account (sign up at workos.com).
- API keys (Publishable and Secret) from the WorkOS dashboard.
- A web framework of your choice (the examples use Flask and Express).
- HTTPS enabled locally (e.g., using
ngrok)—IdPs reject non‑secure callbacks.
Once you’ve collected the keys, store them securely in environment variables:
# .env file
WORKOS_API_KEY=sk_test_XXXXXXXXXXXXXXXX
WORKOS_CLIENT_ID=client_XXXXXXXXXXXXXXXX
WORKOS_REDIRECT_URI=https://your-ngrok-subdomain.ngrok.io/auth/callback
Step 1: Install the SDK
WorkOS offers official SDKs for Python, Node.js, Ruby, and Go. Choose the one that matches your stack.
For Python (Flask example):
pip install workos
For Node.js (Express example):
npm install @workos-inc/node
Step 2: Configure the SDK
Initialize the client with your secret key and set the redirect URI. Keep this configuration in a dedicated module so you can import it everywhere.
Python (Flask)
# workos_client.py
import os
from workos import WorkOS
workos = WorkOS(api_key=os.getenv("WORKOS_API_KEY"))
workos.redirect_uri = os.getenv("WORKOS_REDIRECT_URI")
Node.js (Express)
// workosClient.js
const WorkOS = require("@workos-inc/node").WorkOS;
const workos = new WorkOS(process.env.WORKOS_API_KEY);
workos.redirectURI = process.env.WORKOS_REDIRECT_URI;
module.exports = workos;
Step 3: Create the Login Endpoint
The first user‑facing route redirects the browser to WorkOS’s hosted login page. You can optionally pass a connection ID to pre‑select a specific IdP, but most SaaS products let the user choose.
Flask Example
# app.py (excerpt)
from flask import Flask, redirect, request, session, url_for
from workos_client import workos
app = Flask(__name__)
app.secret_key = "super‑secret‑session‑key"
@app.route("/login")
def login():
# Generate a state token to mitigate CSRF
state = workos.utils.generate_state()
session["workos_state"] = state
# Build the login URL
login_url = workos.sso.get_authorization_url(
client_id=os.getenv("WORKOS_CLIENT_ID"),
redirect_uri=os.getenv("WORKOS_REDIRECT_URI"),
state=state,
# optional: domain_hint="example.com"
)
return redirect(login_url)
Express Example
// server.js (excerpt)
const express = require("express");
const session = require("express-session");
const workos = require("./workosClient");
require("dotenv").config();
const app = express();
app.use(session({
secret: "super‑secret‑session‑key",
resave: false,
saveUninitialized: true,
}));
app.get("/login", (req, res) => {
const state = workos.utils.generateState();
req.session.workosState = state;
const loginUrl = workos.sso.getAuthorizationUrl({
clientId: process.env.WORKOS_CLIENT_ID,
redirectUri: process.env.WORKOS_REDIRECT_URI,
state,
// optional: domainHint: "example.com"
});
res.redirect(loginUrl);
});
Pro tip: Persist the state token in a short‑lived cache (Redis) if you run multiple server instances. This avoids session‑affinity issues.
Step 4: Handle the Callback
After the user authenticates with their IdP, WorkOS redirects back to the redirect_uri with a code and the original state. Exchange the code for a user profile.
Flask Callback
@app.route("/auth/callback")
def auth_callback():
# Verify state token
received_state = request.args.get("state")
if received_state != session.get("workos_state"):
return "Invalid state", 400
# Exchange code for profile
code = request.args.get("code")
profile = workos.sso.get_profile_and_token(code)
# At this point you have:
# profile.id, profile.email, profile.first_name, profile.last_name, etc.
# Store or create a local user record
user = get_or_create_user(profile)
# Log the user in (Flask-Login example)
login_user(user)
return redirect(url_for("dashboard"))
Express Callback
app.get("/auth/callback", async (req, res) => {
const { state, code } = req.query;
// Verify state token
if (state !== req.session.workosState) {
return res.status(400).send("Invalid state");
}
try {
const profile = await workos.sso.getProfileAndToken({ code });
// profile.id, profile.email, profile.first_name, profile.last_name ...
const user = await getOrCreateUser(profile);
req.session.userId = user.id; // simple session login
res.redirect("/dashboard");
} catch (err) {
console.error("WorkOS callback error:", err);
res.status(500).send("Authentication failed");
}
});
The profile object contains enough data to either match an existing account or provision a new one. Most SaaS products use the IdP‑provided email as the unique identifier.
Step 5: Provision Users with SCIM (Optional but Powerful)
If you want your SaaS to stay in sync with the enterprise directory—adding new hires automatically and removing departing employees—you’ll need SCIM. WorkOS offers a hosted SCIM endpoint that you can enable with a single toggle.
- Enable “SCIM Provisioning” in the WorkOS dashboard.
- Define a webhook URL in your SaaS that receives
POST /scim/v2/Usersevents. - Map the incoming SCIM payload to your internal user model.
SCIM Webhook Example (Node.js)
app.post("/scim/v2/Users", express.json(), async (req, res) => {
const event = req.body; // WorkOS sends a standard SCIM payload
if (event.operation === "create") {
await createUserFromScim(event);
} else if (event.operation === "delete") {
await deactivateUser(event.id);
} else if (event.operation === "replace") {
await updateUser(event);
}
// Respond with 200 to acknowledge receipt
res.sendStatus(200);
});
Once enabled, any change in the corporate directory instantly reflects in your SaaS—no manual admin work required.
Pro tip: Store the SCIM id returned by WorkOS alongside your internal user ID. It makes future de‑provisioning idempotent and safe.
Real‑World Use Cases
1. B2B Analytics Platform – A startup needed to onboard enterprise customers quickly. By integrating WorkOS, they reduced the onboarding timeline from 2 weeks (manual SAML) to 1 day (self‑service SSO). The analytics dashboard now displays a “Connected to Company X” banner, reinforcing trust.
2. HR SaaS with Dynamic Permissions – The product ties user roles to Azure AD groups. WorkOS’s group‑claims feature lets the app read the groups attribute from the IdP token and map it to internal permission sets without a separate API call.
3. Multi‑Tenant Project Management Tool – Using WorkOS’s hosted login, the product offers a single URL (login.myapp.com) that automatically detects the customer’s domain (via the domain_hint) and routes them to the correct IdP. This “domain‑aware” flow eliminates the need for a “Select Your Company” dropdown.
Advanced Customizations
WorkOS is flexible enough to handle edge cases. Below are a few patterns you might encounter.
Custom Branding on the Hosted Login Page
- Upload your logo and set brand colors in the WorkOS dashboard.
- Pass a
login_hintparameter to pre‑populate the email field.
Example (Python):
login_url = workos.sso.get_authorization_url(
client_id=os.getenv("WORKOS_CLIENT_ID"),
redirect_uri=os.getenv("WORKOS_REDIRECT_URI"),
state=state,
login_hint="alice@yourcompany.com"
)
Handling Multiple IdPs per Tenant
Some enterprises use both Okta and Azure AD for different divisions. WorkOS lets you create multiple connections and store the connection ID per tenant.
# When rendering the login button
connection_id = tenant.workos_connection_id # fetched from your DB
login_url = workos.sso.get_authorization_url(
client_id=os.getenv("WORKOS_CLIENT_ID"),
redirect_uri=os.getenv("WORKOS_REDIRECT_URI"),
state=state,
connection=connection_id
)
Enforcing MFA via WorkOS Risk Engine
WorkOS can flag high‑risk logins (new device, unusual location). You can inspect the risk_score in the profile response and prompt for an additional step.
profile = workos.sso.get_profile_and_token(code)
if profile.risk_score > 80:
# Redirect to a secondary verification flow
return redirect(url_for("mfa_challenge"))
Testing Your Integration
Before you push to production, run through these sanity checks:
- Happy Path: Log in with a test Okta account, verify the user record is created, and confirm the session persists.
- State Mismatch: Manually tamper with the
statequery param and ensure your app rejects the request. - SCIM Sync: Add a user in Azure AD, watch the webhook fire, and confirm the user appears in your SaaS.
- Logout Flow: Invalidate the session and optionally call WorkOS’s
/logoutendpoint to terminate the IdP session.
WorkOS provides a sandbox environment with mock IdPs, perfect for CI pipelines. Use the WORKOS_API_BASE_URL environment variable to point your SDK at the sandbox.
Performance & Scaling Considerations
Because the heavy lifting (authentication, token validation) happens on WorkOS’s servers, your SaaS only incurs a lightweight HTTP call per login. Still, keep these best practices in mind:
- Cache the JWKs: WorkOS signs tokens with RSA keys. Cache the JSON Web Key Set for up to 24 hours to avoid repeated network fetches.
- Rate‑limit callback endpoints: Prevent denial‑of‑service attacks on
/auth/callbackby applying a modest request‑per‑minute limit. - Stateless sessions: Consider JWT‑based sessions after the initial SSO exchange to reduce DB reads.
Security Checklist
Even though WorkOS handles most security concerns, you still own the surrounding infrastructure.
- Serve all endpoints over HTTPS (including local development with
ngrok). - Validate the
statetoken to prevent CSRF. - Store the WorkOS secret key in a secret manager (AWS Secrets Manager, Vault, etc.).
- Implement least‑privilege access for the SCIM webhook URL (IP allow‑list, HMAC verification).
- Log audit events when users log in or are provisioned; tie them to WorkOS’s audit API for a unified view.
Monitoring & Observability
WorkOS emits detailed logs and webhook events. Pair them with your own telemetry to get end‑to‑end visibility.
- Metrics: Track
login_success,login_failure, andscim_synccounts. - Tracing: Include the WorkOS request ID in your logs to correlate backend processing with the upstream SSO call.
- Alerts: Set thresholds for abnormal spikes in risk scores or failed login attempts.
Deploying to Production
When you’re ready to go live, swap the sandbox API key for the production one and update the redirect URIs in the WorkOS dashboard to match your production domain.
Don’t forget to:
- Enable “Enforce TLS 1.2+” in the dashboard.
- Review the “Allowed Callback URLs” list—any stray URL could be a phishing vector.
- Run a final security audit on the SCIM webhook (verify HMAC signatures