How to add enterprise SSO to an Express.js API
Add enterprise SSO (SAML and OIDC) to an Express.js API with WorkOS. Step-by-step Node.js code for the login redirect, the callback, and JWT verification on protected API routes.
Somewhere between your first paying customer and your first six-figure contract, a procurement questionnaire lands in your inbox asking whether your app supports SAML single sign-on with the customer's identity provider. It is rarely a nice-to-have. For most enterprise buyers, SSO is a requirement their security team will not waive, and the deal stalls until you can answer yes.
If your backend is Node.js and Express, the good news is that the code is smaller than the reputation suggests. You are not implementing SAML. You are adding a redirect, a callback, and a token check. The hard parts, certificate handling, per-provider quirks, metadata exchange, and the IT admin who configures it on the other end, can be pushed outside your codebase entirely.
This tutorial covers both paths: the one-command version using the WorkOS CLI, and the manual version so you understand exactly what the generated code does. You will finish with a working SAML and OIDC login flow, JWT-protected API routes, and multi-tenant authorization driven by signed token claims.
Enterprise SSO in about 15 minutes
To add enterprise SSO to an Express.js API, you need three pieces: a route that redirects users to an identity provider, a callback route that exchanges the returned authorization code for a user profile and tokens, and middleware that verifies the access token JWT on every protected request. WorkOS gives you all three behind one integration, so a single connection covers Okta, Microsoft Entra ID, Google Workspace, Ping, JumpCloud, and any other SAML or OIDC provider your customers bring.
The WorkOS CLI can generate all three for you, or you can write them by hand in about 40 lines of Express. Both paths are below.
Why enterprise SSO is different from social login
Social login is one integration per provider, and the provider is a company you sign a contract with. Enterprise SSO is one integration per customer, and the configuration lives inside your customer's IT department.
That difference is where the work hides:
- Every enterprise buyer wants their own identity provider, and they will not change it for you. Okta, Entra ID, Google Workspace, Ping, OneLogin, JumpCloud, ADFS, Keycloak, Duo, and a long tail of others all show up in real deals.
- SAML and OIDC are standards in name only. Certificate formats, attribute names, entity IDs, and NameID conventions differ by provider, and the metadata is often pasted in by hand by someone you will never meet.
- The person configuring it is a customer IT admin, not your engineer. If onboarding requires a support ticket and three emails, your enterprise deals slow down.
- Once SSO is live, IT admins immediately ask for the next things: user provisioning and deprovisioning, role mapping, and audit logs.
Building this yourself means maintaining a SAML implementation, a certificate rotation process, and a per-provider debugging playbook. WorkOS abstracts all of it behind one OAuth 2.0 style flow, which is why the Express code below looks like ordinary OAuth even when the user on the other end is authenticating against a 15 year old on-premise ADFS deployment.
What WorkOS gives you out of the box
Before you start
You will need:
- Node.js 18 or later and an existing Express app
- A WorkOS account (free, no credit card)
- Your API key (
sk_test_...) and client ID (client_...) from the dashboard
If you skip the account step, the CLI will provision a temporary unclaimed environment for you and let you claim it later with workos env claim.
Option A: Let the CLI do it (fastest)
From the root of your Express project:
The installer will:
- Detect that you are running Express and identify your package manager
- Resolve your credentials, or provision a temporary environment if you have none
- Configure your redirect URI, CORS origins, and homepage URL in the WorkOS dashboard
- Fetch the current SDK documentation, then generate your login route, callback route, and auth middleware to match your project's existing structure
- Write
WORKOS_API_KEY,WORKOS_CLIENT_ID, and a generatedWORKOS_COOKIE_PASSWORDto.env.local, masking the key in your terminal and redacting it from logs - Validate that the app still builds
By default it works on a new branch and commits the change, so the diff is easy to review. Add --no-branch or --no-commit to change that, or --create-pr to open a pull request.
Then start your app and visit /login. You now have a working enterprise SSO flow. Read on if you want to know exactly what that code does, or if you prefer to write it yourself.
Option B: Write it yourself, step by step
Step 1: Install the Node SDK
You also need cookie-parser to read the session cookie, and csrf-csrf to protect the logout route:
Step 2: Set your environment variables
Get your WORKOS_API_KEY and WORKOS_CLIENT_ID from the WorkOS dashboard homepage.
Generate the cookie password with:
Step 3: Configure your redirects
In the WorkOS dashboard, open Applications, select your application, and go to the Redirects tab. Three settings live here, and all three matter:
- Redirect URI. Add
http://localhost:3000/callback. WorkOS will refuse to redirect anywhere that is not on this list, which is what stops an attacker from pointing your callback at their own server. Wildcards are supported for non-default URIs. - Initiate login URL. Set this to your
/loginendpoint. Sign-in requests are supposed to start at your app, but they do not always: a user might bookmark the AuthKit page, or arrive from a password reset or invitation email. When AuthKit detects a request that did not start at your app, it sends the user here instead. Password reset and invitation details survive the redirect, so this endpoint has to start an AuthKit sign-in rather than render your own login page. - Sign-out URI. Set a default, or users will hit an error when they log out.
Step 4: Add the login route
This is the route that kicks off SSO, and it is the endpoint you registered as the Initiate login URL above.
Passing provider: 'authkit' hands the sign-in screen to AuthKit, which resolves the user's organization for you and routes them to the right identity provider. That is the option that stays free for your first 1 million monthly active users.
Pass organizationId when you already know which customer is signing in, for example on a tenant subdomain or from a "Sign in with SSO" button on your own login page. Leave it out and AuthKit will work out the right connection from the user's email address.
Step 5: Add the callback route
WorkOS redirects here with a one-time authorization code, valid for 10 minutes. Exchange it for the authenticated user, an access token, and a refresh token.
Sealing the session matters. The refresh token can be replayed to mint new access tokens, so the SDK encrypts the pair with your WORKOS_COOKIE_PASSWORD before it ever reaches the browser.
This is also where you reconcile the WorkOS user with your own database. Key your records on user.id, which is stable across sessions and identity providers, rather than on the email address, which an IT admin can change.
Step 6: Protect your API routes
This is the part that makes it an API integration rather than a web app integration. There are two patterns, and which one you want depends on who calls your Express server.
Pattern 1: Cookie sessions, for a browser client on the same site
Then add the middleware to any route that should only be reachable by a signed-in user, and load the session again inside the handler to get the user:
Reloading the session in the handler looks redundant, but it is deliberate. After a refresh, req.cookies still holds the old value for the rest of that request, which is why the middleware redirects to req.originalUrl rather than calling next(). If a redirect is wrong for your client, for example a fetch call that cannot follow one, return a 401 after the refresh instead and let the client retry with the new cookie.
Pattern 2: Bearer tokens, for an SPA, mobile app, or service client
WorkOS access tokens are standard JWTs, signed and published at a JWKS endpoint, so your API can verify them locally with no network round trip per request. Verify with jose:
The claims you get back are what makes multi-tenant authorization straightforward:
Because org_id and role are signed into the token, tenant scoping and role checks become a property of the request rather than a database lookup:
Keep the access token duration short in the dashboard, under Applications then Sessions, so revoked access and changed roles take effect quickly. Refresh tokens handle the renewal.
Step 7: Sign out
Ending the session at WorkOS as well as in your app is the part people forget. If you only clear your own cookie, the user's identity provider session is still live and the next /login silently signs them back in. That is also why the Sign-out URI from step 3 has to be set: without it, users see an error when they log out.
Logout is a POST rather than a GET so that browser link prefetching cannot sign your users out, and csrf-csrf stops a third-party site from triggering it on their behalf.
Test the whole flow before you have a customer
Every WorkOS staging environment includes a test organization backed by a mock identity provider, with the ID org_test_idp. Point your login route at it:
Follow the redirect in a browser, sign in with the mock IdP, and you should land back on your callback with a real authorization code and a real signed JWT. Nothing is stubbed except the IdP itself.
The Test SSO page in the dashboard also walks through the common login flows, including identity-provider-initiated sign-in, which is the one that tends to break in production.
Onboard your first real customer
You do not need to configure your customer's Okta or Entra ID tenant yourself. Create the organization and generate an Admin Portal link:
That single command creates the organization, adds the domain, creates the roles, and returns an Admin Portal link. Send the link to the customer's IT admin. They walk through a hosted, provider-specific setup flow, paste in their metadata, and the connection goes live. You get a webhook when it activates.
When something does go wrong, and with enterprise SAML something eventually does:
That inspects the connection state and recent authentication events, and flags inactive connections, which turns "SSO is broken" into a specific answer.
What this costs
- AuthKit is free for up to 1 million monthly active users, then $2,500 per additional million. A user counts as active if they take any action, such as signing up, signing in, or updating a profile, in the calendar month.
- Enterprise SSO connections on the standalone SSO API are $125 per connection per month, with automatic volume discounts that reach 48% off at 51 or more connections. One connection covers an entire customer, regardless of how many of their employees sign in or which identity provider they use.
- Staging is free. Every WorkOS product is available in staging at no cost, and you do not need a credit card until you go to production.
Common pitfalls
- Validating on email domain instead of organization ID. Do not decide which tenant a user belongs to by parsing their email domain. Enterprises invite guest users and contractors from outside their corporate domain all the time. Check the
org_idclaim orprofile.organizationIdinstead. - Long-lived access tokens. A 24 hour access token means a revoked user keeps API access for up to 24 hours. Keep the duration short and lean on refresh tokens.
- Forgetting identity-provider-initiated login. Enterprise users often start from their Okta or Entra dashboard tile, not from your app. That flow never hits your
/loginroute, so your callback has to work without any state you set beforehand. - Skipping the redirect URI allowlist. If it is not registered in the dashboard, WorkOS will not redirect to it. This is a feature, not a bug.
- Storing refresh tokens unencrypted in a cookie. Use
sealSession: trueand let the SDK encrypt them, or keep refresh tokens server side.
Frequently asked questions
- Can I add enterprise SSO to an existing Express API that already has its own login?Yes. Use the standalone SSO API (
workos.sso.getAuthorizationUrlandworkos.sso.getProfileAndToken) rather than AuthKit. It handles only the identity provider handshake and returns a normalized profile, which you map onto your existing user records. Your current password or social login keeps working alongside it. - Which identity providers does WorkOS support?Any provider that speaks SAML or OIDC, which in practice is all of them. WorkOS publishes dedicated setup guides for more than 30 providers, including Okta, Microsoft Entra ID, Google Workspace, PingOne, PingFederate, OneLogin, JumpCloud, Microsoft AD FS, Auth0, Keycloak, Duo, CyberArk, Cloudflare, Salesforce, Oracle, Login.gov, Shibboleth, miniOrange, LastPass, NetIQ, ClassLink, and VMware, plus generic SAML and generic OIDC connectors.
- How long does this take?The CLI path takes a few minutes. Writing it by hand takes about 15, and roughly 40 lines of Express code. Setting up your first real customer connection is a task for their IT admin, not for you, via the Admin Portal.
- Do I need to implement SAML myself?No. You never parse a SAML assertion, validate a signature, or rotate a certificate. Your Express app sees an OAuth 2.0 style redirect, an authorization code, and a JWT, no matter which protocol the customer's provider speaks.
- How do I handle a user who belongs to more than one organization?The access token carries a single
org_id, the organization selected at sign-in. To switch tenants, call the authenticate-with-refresh-token endpoint with anorganization_idparameter. If the user is authorized for that organization, you get a new access token with the updatedorg_id,role, andpermissions. - Is the WorkOS free tier really free for 1 million users?Yes, for AuthKit. AuthKit covers up to 1 million monthly active users at no cost, including social login, email and password, Magic Auth, MFA, passkeys, RBAC, and just-in-time provisioning. Enterprise SSO connections on the standalone SSO API are billed per connection.
- Does this work with TypeScript, Fastify, or NestJS?Yes.
@workos-inc/nodeships TypeScript types, and the flow is framework-agnostic: a redirect, a callback, and JWT verification middleware. The CLI supports 15 frameworks and also covers Next.js, Django, Rails, Go, ASP.NET Core, Spring Boot, Phoenix, and Laravel.
Next steps
Once SSO is live, your enterprise buyers will ask for the rest of the checklist, and it is the same organization object and the same API:
- Directory Sync for SCIM provisioning and deprovisioning, so leavers lose access automatically
- RBAC for roles and permissions, already flowing through the JWT claims you verified above
- Audit Logs for the compliance events every security review asks about
- Admin Portal so IT admins configure all of it themselves
- Radar for real-time protection against bots, credential stuffing, and fraudulent signups, sitting in front of the same login flow you just built. The first 1,000 checks are free
Start with npx workos@latest install, or read the Single Sign-On docs and the AuthKit Node.js guide.