In this article
August 14, 2025
August 14, 2025

How to integrate Okta SAML SSO and SCIM in one day

Learn how to set up Okta SAML Single Sign-On (SSO) and SCIM user provisioning with WorkOS in just one day using Node.js, enabling secure authentication and automated user management for your enterprise customers.

In modern enterprise applications, two integrations stand out as essential: Single Sign-On (SSO) for frictionless authentication and SCIM provisioning for automated user lifecycle management. SSO ensures that employees can log in securely via their company’s identity provider without remembering yet another password. SCIM streamlines user administration by automatically creating, updating, and deactivating accounts as changes occur in the company directory.

WorkOS makes it possible to implement both in hours instead of weeks. Using Node.js and the WorkOS SDK, you can set up:

  • Okta SAML SSO: Enable secure, standards-compliant authentication for enterprise customers through Okta, without dealing directly with SAML assertions, XML parsing, or signature validation.
  • Okta SCIM provisioning: Automate user and group management by syncing data directly from Okta into your application, eliminating manual account handling and reducing the risk of errors.

In this guide, you’ll learn how to integrate Okta SAML SSO and Okta SCIM provisioning in a single day, giving your app enterprise-grade authentication and user management in one streamlined process.

We will use Node.js for this tutorial, but you can find snippets for the language of your choice in our quickstarts:

Let’s dive in.

Prerequisites

To follow this tutorial, you will need the following:

  • An Okta account (with access to the admin dashboard).
  • A WorkOS account.
  • A Node.js app (Node 16 or higher).

Step 1: Install the SDK

We will start by installing the WorkOS Node SDK. Choose your preferred package manager and run one of the following commands:

	
# npm
npm install @workos-inc/node

# yarn
yarn add @workos-inc/node

# pnpm
pnpm add @workos-inc/node
	

Step 2: Set secrets

To make calls to WorkOS, you must authenticate using the WorkOS API key and client ID. Copy these values from the WorkOS dashboard.

Store the values as managed secrets and pass them to the SDK either as environment variables or directly in your app’s configuration.

!!For more info on how to handle secrets safely see Best practices for secrets management.!!

Step 3: Configure the Okta connection

At WorkOS, SSO and SCIM connections are enabled and configured at the organization level. An organization is a collection of users that also acts as a container for enterprise features (like SSO). By enabling an SSO connection for a specific organization, you enable the feature for all users who are members of this organization. This way, you can enable features like forcing all users who use a specific email domain to use a specific SSO connection.

!!For more information on organizations and how to use them, see Model your B2B SaaS with organizations.!!

First, we will configure Okta SSO and SCIM at the WorkOS dashboard, and then we will move on to the Okta admin dashboard to finish the configuration.

WorkOS configuration

  1. Go to WorkOS dashboard > Organizations.
  2. Pick (or create) the organization for which you'd like to configure an Okta connection.
  3. Select “Configure manually” under the “Single Sign-On” section.
  4. Pick “Okta” from the dropdown list and click “Create Connection”.
  5. Copy the SP Entity ID and the ACS URL values from the "Service Provider Details" section. We will use these values soon.
  6. We will now configure SCIM. Go back to the organization and select “Configure manually” under the “Directory Sync” section.
  7. Enter a name for the directory and click “Create Directory”. Your Entra ID directory sync has now been created successfully with an Endpoint and a Bearer Token. Copy these values, we will use them soon.

Okta configuration

To configure the connection on Okta’s side, follow these steps:

  1. Log in to Okta and go to the admin dashboard.
  2. Go to “Applications” and if your application is already created, select it from the list of applications, otherwise create a new one.
  3. Input the ACS URL from your WorkOS dashboard as the “Single Sign-On URL” and the SP Entity ID as the “Audience URI (SP Entity ID)”.
  4. Scroll down to the “Attribute Statements” section and use the “Add Another” button to add the following key-value pairs.
    • iduser.id
    • emailuser.email
    • firstNameuser.firstName
    • lastNameuser.lastName
  5. Add users and/or groups to the Okta app.
  6. Click on the “Sign On” tab of the SAML app, click the “Actions” dropdown for the correct certificate and select “View IdP Metadata”. A separate tab will open. Copy the link in the browser.
  7. Back in the WorkOS dashboard, click on “Edit Configuration” in the “Identity Provider Configuration” section of the Connection. Input the Metadata URL and click “Save Metadata Configuration”. Your connection will then be linked and good to go!
  8. We will now configure SCIM. Go back to the Okta dashboard, select your application, and if you haven’t created a SCIM application in Okta, select “Browse App Catalog”.
  9. Search for “SCIM 2.0 Test App (OAuth Bearer Token)” and select it.
  10. On the following page, click “Add Integration”.
  11. Enter a descriptive App name, then click “Next”.
  12. In your application’s Enterprise Okta admin panel, click the “Provisioning” tab. Then, click “Configure API Integration”.
  13. Check “Enable API Integration”.
  14. Copy and paste the Endpoint from your WorkOS dashboard in the SCIM 2.0 Base URL field.
  15. Then, copy and paste the Bearer Token into the OAuth Bearer Token field.
  16. Click “Test API Credentials”, and then click “Save”.
  17. In the “To App” navigation section, check to enable:
    • Create Users
    • Update User Attributes
    • Deactivate Users
  18. Click “Save”.
  19. To assign users to the SAML Application, navigate to the “Assignments” tab, from the “Assign” dropdown, select “Assign to People”. Select users you’d like to provision and select “Assign”.
  20. To push groups in order to sync group membership, navigate to the “Push Groups” tab, from the “Push Groups” dropdown, select: “Find groups by name”. Search for the group you’d like to push and select it. Make sure the box is checked for “Push Immediately” and click “Save”.
  21. In the WorkOS dashboard, you should now see the users and groups synced over.

For more detailed steps and screenshots, see the integration guides:

Step 4: Configure a redirect URI

A redirect URI is the endpoint where the users are redirected after they sign in. We’ll create this endpoint in a bit. For the time being, we need to add the URI in the Redirects section of the WorkOS dashboard.

While wildcards in your URIs can be used in the staging environment, they and query parameters cannot be used in production. When users sign out of their application, they will be redirected to your app’s homepage, which is configured in the same dashboard area.

Step 5: Set up the frontend

We are ready to start adding code. In this tutorial we will use React in the frontend but you can use instead Next.js, Remix, or vanilla JS.

To set up the frontend create a simple page with login and logout links. Create a new React app if you don’t have one already, and add the following code to your App.js:

	
export default function App() {
  return (
    <div className="App">
      <h1>SSO example</h1>
      <p>
        <a href="/login">Sign in</a>
      </p>
      <p>
        <a href="/logout">Sign out</a>
      </p>
    </div>
  );
}
	

Step 6: Set up the backend

The authentication process happens in two steps. First, we will start the authentication process by redirecting the user to the identity provider. After the user authenticates, they will be redirected back to the app which will finalize the process by getting the user’s profile an an access token.

At this point you have to decide whether you will use AuthKit, a customizable login box powered by WorkOS and Radix, or build your own login box. Depending on the choice you will use a different API:

SSO with AuthKit

Initiate login

When the user clicks “Sign in”, we need to start the authentication process. We will use the  getAuthorizationUrl  method to generate the authorization URL where the user will be redirected to authenticate.

Add the following code to server.js:

	
const express = require('express');
const { WorkOS } = require('@workos-inc/node');

const app = express();

const workos = new WorkOS(process.env.WORKOS_API_KEY, {
  clientId: process.env.WORKOS_CLIENT_ID,
});

app.get('/login', (req, res) => {
  const authorizationUrl = workos.userManagement.getAuthorizationUrl({
    // Specify that we'd like AuthKit to handle the authentication flow
    provider: 'authkit',

    // The callback endpoint that WorkOS will redirect to after a user authenticates
    redirectUri: 'http://localhost:3000/callback',
    clientId: process.env.WORKOS_CLIENT_ID,
  });

  // Redirect the user to the AuthKit sign-in page
  res.redirect(authorizationUrl);
});
	

After the user authenticates, WorkOS redirects them to the RedirectUri including in the query string the authorization code. The URL would look like this:

	
https://your-app.com/callback?code=g0FGFmNjVmOWIkTGf2PLk4FTYyFGU5
	

Your app needs to extract this code and exchange it for a token (in the next step).

Handle the callback

After the user successfully authenticates, WorkOS will generate a string (the authorization code) and send it back to the app as part of the Redirect URI. The app needs to extract that code and make another call to WorkOS in order to complete the authentication process by exchanging the authorization code for a token and user profile information.

Add the following code to server.js:

	
app.get('/callback', async (req, res) => {
  // The authorization code returned by AuthKit
  const code = req.query.code;
  if (!code) {
    return res.status(400).send('No code provided');
  }

  const { user, access_token } = await workos.userManagement.authenticateWithCode({
    code,
    clientId: process.env.WORKOS_CLIENT_ID,
  });

  // Use the information in `user` for further business logic.

  // Redirect the user to the homepage
  return res.redirect('/');
});
	

The user has now successfully logged in with Okta SSO. This is what the response looks like:

	
{
  "user": {
    "object": "user",
    "id": "user_01E4ZCR3C56J083X43JQXF3JK5",
    "email": "marcelina.davis@example.com",
    "first_name": "Marcelina",
    "last_name": "Davis",
    "email_verified": true,
    "profile_picture_url": "https://workoscdn.com/images/v1/123abc",
    "created_at": "2021-06-25T19:07:33.155Z",
    "updated_at": "2021-06-25T19:07:33.155Z"
  },
  "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG",
  "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0",
  "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK",
  "impersonator": {
    "email": "admin@foocorp.com",
    "reason": "Investigating an issue with the customer's account."
  }
}
	

The user object can be used for further business login like personalizing the UI for the user.

The response also includes an access token and a refresh token. These two tokens can be used to manage the user’s session without asking them to authenticate all the time. The access token is short-lived and allows an application to access resources on a user’s behalf, while the refresh token, which lives a bit longer, can be used to get a new access token when that expires.

Both tokens should be handled and stored securely since if an attacker manages to obtain a user's token, they can impersonate the user and gain unauthorized access to protected resources. WorkOS SDKs use sealed sessions (i.e., sessions encrypted with a strong password) to keep tokens safe. For more information, see Handle the user session.

SSO without AuthKit

Initiate login

When the user clicks “Sign in”, we need to start the authentication process. We will use the getAuthorizationUrl method to generate the authorization URL where the user will be redirected to authenticate.

Add the following code to server.js:

	
const express = require('express');
const { WorkOS } = require('@workos-inc/node');

const app = express();

const workos = new WorkOS(process.env.WORKOS_API_KEY);
const clientId = process.env.WORKOS_CLIENT_ID;

app.get('/login', (_req, res) => {
  // The ID of the organization associated with the SSO connection.
  // For testing purposes, use the ID of the Test Organization from the dashboard. 
  // Replace it with the real organization ID when you finish the integration.
  const organization = 'org_test_idp';

  // The callback URI WorkOS should redirect to after the authentication
  const redirectUri = 'http://localhost:3000/callback';

  const authorizationUrl = workos.sso.getAuthorizationUrl({
    organization,
    redirectUri,
    clientId,
  });

  res.redirect(authorizationUrl);
});
	

After the user authenticates, WorkOS redirects them to the redirectUri including in the query string the authorization code. The URL would look like this:

	
https://your-app.com/callback?code=g0FGFmNjVmOWIkTGf2PLk4FTYyFGU5
	

Your app needs to extract this code and exchange it for a token (in the next step).

Handle the callback

After the user successfully authenticates, WorkOS will generate a string (the authorization code) and send it back to the app as part of the Redirect URI. The app needs to extract that code and make another call to WorkOS in order to complete the authentication process by exchanging the authorization code for a token and user profile information.

Add the following code to server.js:

	
const express = require('express');
const { WorkOS } = require('@workos-inc/node');

const app = express();
const workos = new WorkOS(process.env.WORKOS_API_KEY);
const clientId = process.env.WORKOS_CLIENT_ID;

app.get('/callback', async (req, res) => {
  // Extract the authorization code from the URL
  const { code } = req.query;
  if (!code) {
    return res.status(400).send('No code provided');
  }

  const { profile, access_token } = await workos.sso.getProfileAndToken({
    code,
    clientId,
  });

  // Use the information in `profile` for further business logic.

  // Redirect the user to the homepage
  res.redirect('/');
});
	

The user has now successfully logged in with Okta SSO. This is what the response looks like:

	
{
  "access_token": "01DMEK0J53CVMC32CK5SE0KZ8Q",
  "profile": {
    "object": "profile",
    "id": "prof_01DMC79VCBZ0NY2099737PSVF1",
    "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5",
    "connection_type": "OktaSAML",
    "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
    "email": "todd@example.com",
    "first_name": "Todd",
    "last_name": "Rundgren",
    "idp_id": "00u1a0ufowBJlzPlk357",
    "role": { "slug": "admin" },
    "raw_attributes": {}
  }
}
	

The profile object can be used for further business login like personalizing the UI for the user.

Step 7: Test the connection

To confirm your Single Sign-On integration works correctly you can use the default Test Organization and active SSO connection you can find in the WorkOS dashboard.

To get started, go to the WorkOS dashboard and navigate to the Test SSO page. This page outlines a number of different SSO scenarios you can follow and provides all the necessary information to complete the tests.

  • Service provider-initiated SSO: The test simulates users initiating authentication from your sign-in page. In this scenario, the user enters their email in your app, gets redirected to the identity provider, and then is redirected back to your application. You can do this test with AuthKit or with standalone SSO.
  • Identity provider-initiated SSO: This test simulates users initiating authentication from their identity provider. It is a common login flow that developers forget to consider. In the scenario, users log in to the identity provider directly, select your application from their list of SSO-enabled apps, and are redirected to your application upon successful authentication.

Other options are guest email domain, which simulates users authenticating with an email domain different from the verified domain of the test organization, and error response, which simulates a generic error response from the user’s identity provider.

To run all these steps follow the on-screen instructions.

Step 8: Sync users and groups to your app

Once you've connected Okta to WorkOS, any time a directory-related resource (like a user or group) is created, updated, or deleted in Okta, WorkOS will generate a corresponding event. These events allow your app to stay in sync with the identity provider in real time, ensuring user data is always up to date.

Each WorkOS event has the following structure:

Attribute Description
event A string that distinguishes the event type.
id Unique identifier for the event.
data Event payload. Payloads match the corresponding API objects.
created_at Timestamp of when the event occurred.

For example, this is an event about the creation of a new user:

  
{
  "event": "dsync.user.created",
  "id": "event_07FKJ843CVE8F7BXQSPFH0M53V",
  "data": {
    "id": "directory_user_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74",
    "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
    "idp_id": "8931",
    "emails": [
      {
        "primary": true,
        "type": "work",
        "value": "lela.block@example.com"
      }
    ],
    "first_name": "Lela",
    "last_name": "Block",
    "username": "lela.block@example.com",
    "state": "active",
    "created_at": "2021-06-25T19:07:33.155Z",
    "updated_at": "2021-06-25T19:07:33.155Z",
    "custom_attributes": {
      "department": "Engineering",
      "job_title": "Software Engineer"
    },
    "role": { "slug": "member" },
    "raw_attributes": {}
  },
  "created_at": "2021-06-25T19:07:33.155Z"
}
  

The different directory events are:

Event Description
dsync.activated Triggered when a directory is activated.
dsync.deleted Triggered when a directory is deleted. The state attribute indicates directory state before deletion.
dsync.group.created Triggered when a directory group is created.
dsync.group.deleted Triggered when a directory group is deleted.
dsync.group.updated Triggered when a directory group is updated.
dsync.group.user_added Triggered when a directory group user is added.
dsync.group.user_removed Triggered when a directory group user is removed.
dsync.user.created Triggered when a directory user is created.
dsync.user.deleted Triggered when a directory user is deleted. The state attribute indicates directory user state at time of deletion.
dsync.user.updated Triggered when a directory user is updated.

!!For details and sample payloads on each event, see Understanding the events lifecycle.!!

Sync events to your app

After WorkOS generates these events, your app needs to consume them to stay in sync with the latest changes. You can do this in one of two ways:

  • By polling the Events API for new events, or
  • By setting up webhooks to receive them automatically in real time.

Either approach ensures that your app reflects the current state of the directory.

With the events API, your application retrieves events from WorkOS. It provides a consistent, ordered stream of immutable events via a paginated endpoint, allowing your application to pull and process changes at its own pace. This architecture not only ensures data integrity and easier error recovery, but also simplifies debugging and audit logging.

With webhooks, WorkOS pushes events to your app in real-time by invoking an endpoint you host. Although webhooks are popular, they are also prone to issues like missed deliveries, out-of-order events, and scalability bottlenecks (for more, see Why you should rethink your webhook strategy). We recommend using the events API instead.

Aspect Events API Webhooks
Timing Controlled by your app. Your server can process events at its own pace. Real-time. Webhooks trigger as soon as an event occurs.
Order A consistent order is guaranteed. No guarantee of order on receipt. Events contain timestamps to determine order.
Reconciliation Replayable. Can go back to a specific point in time and reprocess events. Failed requests are retried with exponential back-off for up to 3 days.
Security Authentication, confidentiality, and integrity protection by default. You must expose a public endpoint and validate webhook signatures.

Sync data using the events API

To start consuming events, you first need to pick a starting place in the data set. For that, you need a cursor.

A cursor is a bookmark to track your app’s position in the events list. The very first call to the events API won’t have a cursor. Subsequent requests to WorkOS should include the updated cursor using the after parameter. You will need to update and store your cursor after processing an event.

Determine the event types you want to pull, and call the listEvents method. In this example, we are pulling the first 100 events:

  
require "workos"

events = WorkOS::Events.list_events(
  events: [
    "dsync.activated",
    "dsync.deleted",
    "dsync.user.created",
    "dsync.user.updated",
    "dsync.user.deleted"
  ],
  limit: 100
)
  

This is an example response (one user created, and one updated):

  
{
  "data": [
    {
      "event": "dsync.user.created",
      "id": "event_07FKJ843CVE8F7BXQSPFH0M53V",
      "data": {
        "id": "directory_user_01E1X1B89NH8Z3SDFJR4H7RGX7",
        "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74",
        "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
        "idp_id": "8931",
        "emails": [
          {
            "primary": true,
            "type": "work",
            "value": "lela.block@example.com"
          }
        ],
        "first_name": "Lela",
        "last_name": "Block",
        "username": "lela.block@example.com",
        "state": "active",
        "created_at": "2021-06-25T19:07:33.155Z",
        "updated_at": "2021-06-25T19:07:33.155Z",
        "custom_attributes": {
          "department": "Engineering",
          "job_title": "Software Engineer"
        },
        "role": { "slug": "member" },
        "raw_attributes": {}
      },
      "created_at": "2021-06-25T19:07:33.155Z"
    },
    {
      "event": "dsync.user.updated",
      "id": "event_08GHJ944DVE9G8CXRTPGI1N64W",
      "data": {
        "id": "directory_user_01E2Y2C90OI9H4TGFSK5H9TYU8",
        "directory_id": "directory_01ECBZ5PW0RNX58HY984IDCZ85",
        "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
        "idp_id": "8931",
        "emails": [
          {
            "primary": true,
            "type": "work",
            "value": "jacob.morris@example.com"
          }
        ],
        "first_name": "Jacob",
        "last_name": "Morris",
        "username": "jacob.morris@example.com",
        "state": "active",
        "created_at": "2021-05-15T13:00:00.000Z",
        "updated_at": "2021-07-01T10:45:00.000Z",
        "custom_attributes": {
          "department": "Marketing",
          "job_title": "Marketing Manager"
        },
        "role": { "slug": "admin" },
        "raw_attributes": {}
      },
      "created_at": "2021-07-01T10:45:00.000Z"
    }
  ],
  "list_metadata": {
    "after": "event_08GHJ944DVE9G8CXRTPGI1N64W"
  }
}
  

Retrieve more events from the API using cursor pagination. To fetch the next set of events, provide the ID of the latest processed event in the after parameter.

For more info, see the Sync data using the Events API quickstart.

Step 9 (optional): Get specific users and groups

The API offers some more endpoints you can use for specific use cases:

Step 10 (optional): Stream events to Datadog

WorkOS supports real-time streaming of events to Datadog. By analyzing WorkOS activity directly in Datadog, you are able to:

  • View trends in user sign-ins, user growth, new SSO connections and more.
  • Debug customer issues related to sign-in, email verification, password resets and more.
  • Generate reports of user activity per customer organization.
  • Set alerts for unexpected activity, such as sudden spike in failed password attempts.

To set up real-time streaming of WorkOS events to Datadog, follow these steps:

  1. Create a new Datadog API key to give WorkOS permission to send event activity as logs to your Datadog account.
  2. Configure event streaming in the WorkOS dashboard using the Datadog API key.
  3. Add the WorkOS Datadog dashboard to your Datadog account.

For detailed instructions and screenshots, see the docs: Stream events to Datadog.

Next steps

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.