# Adding Okta SSO to a React app: SAML and OIDC walkthrough - Part 3

> Part 3 of 3. Start with [Adding Okta SSO to a React app: SAML and OIDC walkthrough](https://clerk.com/articles/adding-okta-sso-to-a-react-app-saml-and-oidc-walkthrough.md).

**How do I test Okta SAML, compare managed SSO platforms, and troubleshoot a React integration?**

Test the SAML flow by inspecting the redirects, `SAMLRequest`, `SAMLResponse`, session cookie, and `/me` response. Then decide whether direct OIDC, direct SAML, or a managed auth platform fits your identity-provider, protocol, and provisioning requirements.

This is Part 3 of the series: SAML validation, managed-platform comparison, Clerk's fit for Okta enterprise SSO, troubleshooting, and FAQs.

### Step 8: Test the SAML flow end to end

Run the verification in order. This is the SP-initiated flow, so it parallels the OIDC test in Walkthrough A.

1. Start both dev servers (Vite and the Express SP). Load the Vite origin and confirm the proxy forwards: hitting `/me` should reach the backend, not 404 on the SPA.
2. Click **"Sign in with SSO"**. The `/login` route returns a 302 to Okta carrying a `SAMLRequest`.
3. Authenticate at Okta.
4. Okta HTTP-POSTs the `SAMLResponse` to your ACS (`/login/callback`).
5. The backend validates the assertion, sets the session cookie, and 302-redirects to the SPA.
6. `/me` returns 200 with the user.
7. A protected route renders, and the session survives a page reload.

What to inspect at each hop:

- **Network tab**: the 302s, the decoded `SAMLRequest` and `SAMLResponse`, the `Set-Cookie` header on the callback response, and the `Cookie` header the browser sends on `/me`.
- **DevTools → Application → Cookies**: confirm the `session` cookie exists on your dev origin.
- **SAML-tracer** (a Firefox/Chrome extension): decode the `AuthnRequest` and `SAMLResponse` to read Issuer, Audience, Recipient, and Conditions directly.

The three most common first failures, in order: an **Audience or Recipient (ACS) mismatch** (Okta's Audience URI or Single sign-on URL doesn't match your node-saml `audience` / `callbackUrl`), the **session cookie not reaching the SPA** (the cross-origin trap; switch to the same-origin proxy), and **clock skew** (`NotBefore` / `NotOnOrAfter` outside your `acceptedClockSkewMs`) ([ScaleKit: SAML debugging](https://www.scalekit.com/blog/saml-debugging-handbook-2026-how-to-diagnose-log-and-resolve-sso-failures), [WorkOS: common SAML errors](https://workos.com/blog/saml-assertion-failures-debugging-guide)).

## Option C: Broker Okta through a managed auth platform

A managed auth platform is the better path the moment SAML, multiple customer identity providers, per-tenant routing, or SCIM enters the picture. The platform acts as the service provider, your customers' Okta orgs become "enterprise connections" you configure in a dashboard, and your React app talks only to the platform's SDK. You write no backend SP, validate no XML, and rotate no certificates.

Walkthroughs A and B both add your own Okta org as the app's IdP. That's the single-tenant case. Multi-tenant enterprise SSO, where each B2B customer brings their own Okta or IdP, is a different problem, and it's the one managed platforms are built to solve.

### When direct integration stops being worth it

Hand-rolling OIDC against your own Okta org is fine, and Walkthrough A is the recommended path for that. The calculus changes when any of these become true:

- You need **both SAML and OIDC**, because different customers mandate different protocols.
- You have **multiple customer IdPs**, not just your own org.
- You need **per-tenant routing**: customer A's users go to customer A's Okta, customer B's to customer B's.
- You need **SCIM provisioning**, so deactivating a user in the IdP deactivates them in your app.
- You'd rather not **operate a service provider** at all: standing up an SP, patching the SAML dependency CVEs (the node-saml, xml-crypto, and samlify criticals from Walkthrough B, Step 7), and rotating signing certificates on a schedule.

Any one of those is a reason to look at a managed platform. Two or more, and self-hosting an SP per customer rarely pays for itself.

### How a managed platform changes the React integration

The architecture flips. With a self-hosted SP (Walkthrough B), your backend is the service provider: it holds the SP private key, validates the signed assertion, and mints the session. With a managed platform, the platform is the service provider. It owns the SP duties, and Okta is registered as an enterprise connection inside it.

That collapses the React side back to the simplicity of the OIDC path. Your SPA uses the platform's SDK to start sign-in and read session state, the same shape as a hosted OIDC flow. No backend SP, no XML handling, no certificate rotation, regardless of whether the underlying customer connection is SAML or OIDC. The platform absorbs the protocol difference so your app doesn't see it.

### The options

Clerk, Auth0, and WorkOS all broker Okta as an enterprise connection. They're aimed at different shapes of problem, so compare them on protocol coverage, the multi-tenant model, the pricing model, and developer experience rather than a single score. All pricing below is as of mid-2026 and moves often, so confirm current numbers on each vendor's pricing page.

|                         | Clerk                                                                                                                                                   | Auth0                                                                                                                                        | WorkOS                                                                                    |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Okta SAML               | Named "Okta Workforce" provider ([docs](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/saml/okta.md))                   | Okta as a SAML IdP ([docs](https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/okta))                       | Dedicated "Okta SAML" connection ([docs](https://workos.com/docs/integrations/okta-saml)) |
| Okta OIDC               | Custom OIDC Provider path ([docs](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/oidc/custom-provider.md))              | Named "Okta Workforce" connection (OIDC) ([docs](https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/okta)) | OIDC connection supported                                                                 |
| Multi-tenant model      | Connection scoped to an Organization; email domain is the routing key ([docs](https://clerk.com/docs/guides/organizations/add-members/sso.md))          | Connections enabled per Organization ([docs](https://auth0.com/docs/manage-users/organizations))                                             | Connection per customer environment                                                       |
| SCIM provisioning       | Directory Sync, included with the connection ([docs](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/directory-sync.md)) | Supported (free plan and up, since Feb 2026)                                                                                                 | Directory Sync, priced per connection                                                     |
| Pricing model           | Per-connection, metered tiers; 1 included on paid plans                                                                                                 | 1 free connection, then per-connection add-on                                                                                                | Per-connection, volume tiers                                                              |
| Bundled user management | Yes, full auth and user management                                                                                                                      | Yes                                                                                                                                          | AuthKit, free to 1M MAU                                                                   |

The pricing models differ enough that "cheapest" depends entirely on your connection count and user volume:

- **Clerk.** Pro ($25/mo) and Business ($300/mo) each include 1 enterprise connection and 50,000 monthly retained users (MRU; Clerk meters retained users rather than MAU). Additional connections are metered: $75/mo each for connections 2 to 15, $60/mo for 16 to 100, $30/mo for 101 to 500, and $15/mo beyond 500 ([Clerk pricing](https://clerk.com/pricing)). Development instances get enterprise connections for free, capped at 25 ([Clerk: enterprise connections overview](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/overview.md)).
- **Auth0** (part of Okta since 2021). Since its February 2026 B2B plan update, Auth0's free plan includes one enterprise connection, Self-Service SSO, and SCIM provisioning, so a basic Okta integration can start on the free plan ([Auth0: B2B plans upgraded](https://auth0.com/blog/auth0-b2b-plans-upgraded/)). Beyond that, B2B Essentials is $150/mo (3 enterprise connections) and B2B Professional is $800/mo (5 connections), each starting at 500 MAU, with additional connections $100/mo each, capped at 30 total ([Auth0 pricing](https://auth0.com/pricing)).
- **WorkOS.** SSO connections are $125/mo each for 1 to 15, $100/mo for 16 to 30, and $80/mo for 31 to 50, with the per-connection price falling to $50/mo at higher volumes; Directory Sync (SCIM) is billed on the same per-connection ladder. AuthKit, its user-management layer, is free up to 1 million MAU and is billed separately from connections ([WorkOS pricing](https://workos.com/pricing)).

At a low connection count and moderate usage, Clerk's included connection and 50,000 retained users tend to come in lower; WorkOS gets competitive when you have many connections and want AuthKit's free MAU tier; Auth0 fits teams already standardized on Okta's ecosystem, since Auth0 has been part of Okta since the roughly $6.5 billion acquisition closed in May 2021 ([Auth0: Okta acquisition close](https://auth0.com/blog/okta-acquisition-announcement/)). Run your own numbers; the break-even shifts with both inputs.

### Where Clerk fits

Clerk is the platform we build, so here's the honest scope of what it does for Okta, with every claim linked.

Clerk supports Okta SAML through a named **Okta Workforce** provider: you select it, Clerk gives you an ACS URL and an Audience URI (SP Entity ID), and your Okta admin pastes them into a SAML app ([Clerk: Okta Workforce SAML connection](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/saml/okta.md)). Clerk supports Okta OIDC through its **Custom OIDC Provider** path: you supply Okta's discovery endpoint, client ID, and secret ([Clerk: custom OIDC provider](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/oidc/custom-provider.md)). Clerk performs the service-provider duties on its side, so your React app never validates an assertion.

> Clerk has a _named_ Okta provider for SAML ("Okta Workforce"), but **not** for OIDC. Okta OIDC goes through the generic **Custom OIDC Provider** path (discovery endpoint + client ID + client secret), not a dedicated "Okta" OIDC button. If you're looking for a named Okta OIDC provider in the dashboard, that's why you won't find one.

For multi-tenant routing, Clerk scopes each enterprise connection to an Organization and uses the email domain as the routing key, so a user at `acme.com` lands on Acme's Okta and a user at `globex.com` lands on Globex's ([Clerk: per-tenant enterprise SSO](https://clerk.com/docs/guides/organizations/add-members/sso.md)). For provisioning, Clerk offers SCIM through [Directory Sync](https://clerk.com/glossary.md#directory-sync), which reached general availability on April 16, 2026, with group-to-role mapping and custom attribute mapping following on May 21, 2026 ([Clerk: Directory Sync](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/directory-sync.md), [Clerk changelog: Directory Sync GA](https://clerk.com/changelog/2026-04-16-directory-sync.md)). The net result: a Vite React SPA can add enterprise Okta SSO using Clerk's React SDK (`@clerk/react`) with no backend service provider.

Clerk doesn't call Okta on every request, which differs from the per-request model people often assume. After Okta authenticates the user, Clerk mints its **own** short-lived session JWT (roughly 60 seconds), and per-request authorization is a local signature verification. The SDK refreshes that token in the background, roughly every 50 seconds ([Clerk: how Clerk works](https://clerk.com/docs/guides/how-clerk-works/overview.md)). Clerk federates the initial sign-in and then runs its own session.

The caveats, stated plainly so you can plan around them:

- **Enterprise connections require a paid plan in production.** Development instances get them free but capped at 25 connections ([Clerk: enterprise connections overview](https://clerk.com/docs/guides/configure/auth-strategies/enterprise-connections/overview.md)).
- **EASIE does not cover Okta.** Clerk's EASIE flow supports Google Workspace and Microsoft Entra ID only, so it is not an Okta path. Okta goes through the SAML or Custom-OIDC enterprise connections above, not EASIE.
- **Okta OIDC is the Custom OIDC Provider path,** not a named provider (see the note above).
- **Per-tenant org linking and custom roles use an add-on.** A standalone enterprise connection is included on the paid plans above, but linking connections to Clerk Organizations (the per-tenant routing described earlier) and custom roles or role-sets require Clerk's **B2B Authentication** add-on ($100/mo, $85/mo billed annually). SCIM provisioning stays included with the connection; only group-to-role mapping reaches into the add-on ([Clerk pricing](https://clerk.com/pricing)).

## OIDC vs SAML vs managed platform: side-by-side

Three paths, one decision. OIDC direct is the lightest if you control the IdP and only speak OIDC. SAML direct is the heaviest, because you build and secure a service provider. A managed platform is light to integrate and the only one of the three that scales cleanly to many customer IdPs.

### Capability and effort matrix

| Capability                           |                       OIDC (direct)                      |                             SAML (direct)                            |            Managed platform            |
| ------------------------------------ | :------------------------------------------------------: | :------------------------------------------------------------------: | :------------------------------------: |
| Runs in the browser only?            |                            Yes                           |                                  No                                  |                   Yes                  |
| Backend required?                    |                            No                            |                                  Yes                                 |                   No                   |
| Refresh / renewal model              |             Refresh-token rotation in the SDK            |              Backend session cookie you issue and expire             |        Platform-managed session        |
| Authorization (group/role claims)    | `groups` claim in the ID token (`idToken.claims.groups`) | Group attribute statement; backend reads/normalizes `profile.groups` |     Platform roles/orgs plus claims    |
| Multi-tenant (many customer IdPs)?   |                            No                            |                     Per-tenant plumbing you build                    |                   Yes                  |
| SCIM provisioning?                   |                            No                            |                                  No                                  |                   Yes                  |
| Certificate / CVE maintenance burden |     Low (SDK verifies tokens against the IdP's JWKS)     |           High (you patch SAML libraries and rotate certs)           |            Platform owns it            |
| Time to first production login       |                          Fastest                         |                                Slowest                               |          Fast once configured          |
| Best-fit trigger                     |            You control the IdP and speak OIDC            |                 A customer or contract mandates SAML                 | Multiple IdPs, both protocols, or SCIM |

A note on the multi-tenant cells: OIDC and SAML direct can technically serve more than one IdP, but only if you build the per-tenant routing, store per-tenant config, and manage each connection yourself. That work is exactly what the managed column hands off, which is why it gets the clean checkmark and the direct paths don't.

### Time to first production login

OIDC direct is fastest: configure the Okta app, drop in `@okta/okta-react`, and you can have a production login working in well under a day because there's no server to build. A managed platform is close behind: most of the time goes into creating the connection and pointing your app at the platform SDK, not into writing auth code. SAML direct is the slowest by a wide margin, because "first login" includes building the service provider, hardening it against XML signature wrapping and XXE, and wiring up session management before you've authenticated a single user.

### Ongoing maintenance

This is where the three paths separate the most. With **SAML direct, you own all of it**: rotating the SP signing certificate, onboarding each new customer IdP by hand, refreshing IdP metadata when certs change, and patching the SAML dependency CVEs (the node-saml, xml-crypto, and samlify criticals are recent and severe, see Walkthrough B, Step 7). With **OIDC direct**, maintenance is light, since the SDK verifies tokens against Okta's published JWKS and there's no SP certificate to rotate. With a **managed platform, the platform owns** certificate rotation, IdP onboarding, metadata refresh, and SAML library patching on its side, which is the single most practical reason teams stop running their own SP.

## Troubleshooting common Okta + React issues

Most Okta + React failures trace back to a handful of mismatches: a redirect URI, a missing Trusted Origin, the wrong React Router pattern, an app assignment, or a cross-origin cookie. Each issue below leads with the symptom, then the cause, then the fix.

### Redirect URI mismatch

The symptom is a blank page, a redirect loop, or an Okta error page right after sign-in instead of a return to your app. The cause is that the redirect URI your app sends does not exactly match a Sign-in redirect URI registered on the Okta app integration. Okta matches the full string: scheme, host, port, and path all have to line up. For a Vite SPA the value is `http://localhost:5173/login/callback` (note `http`, not `https`, on localhost, and port `5173`). Add that exact string under the app's **General → Sign-in redirect URIs**, and set `redirectUri` in your `OktaAuth` config to the same value. A trailing slash, a different port, or `127.0.0.1` instead of `localhost` all count as a mismatch ([Okta Auth Code + PKCE](https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/)).

### CORS or Trusted Origin errors on Vite localhost

The symptom is that sign-in starts fine, the redirect to Okta works, and then the return fails with a browser CORS error in the console and Okta error `E0000021`. The cause is that your dev origin is not registered as a Trusted Origin, so the browser blocks the SDK's cross-origin XHR call to the token endpoint. The `/authorize` step is a full-page navigation and never triggers CORS, which is why sign-in appears to start before it breaks at the `/token` exchange. The fix is to go to **Security → API → Trusted Origins → Add Origin**, set the Origin URL to `http://localhost:5173`, and check **both** CORS and Redirect. The same Trusted Origin is also required for `oktaAuth.signOut()` to work ([Okta enable CORS](https://developer.okta.com/docs/guides/enable-cors/main/), [okta-auth-js issue #605](https://github.com/okta/okta-auth-js/issues/605)).

### Token expired, or silent renewal not firing

The symptom is the user getting logged out unexpectedly, or `authState` going stale after the access token's lifetime. There are three usual causes. First, active `autoRenew` only runs when the SDK service is started, so call `await oktaAuth.start()` once at startup; without it, renewal is passive (renew-on-read) only. Second, refresh tokens are only issued when you request the `offline_access` scope at sign-in, so confirm your `scopes` array includes it (`['openid','profile','email','offline_access']`). Third, a client clock that is skewed from real time makes the SDK think valid tokens are already expired, so check the machine's clock and NTP sync ([okta-auth-js README](https://github.com/okta/okta-auth-js/blob/master/README.md), [Okta refresh tokens](https://developer.okta.com/docs/guides/refresh-tokens/main/)).

### React Router version mismatch (SecureRoute throws on v6/v7)

The symptom is an "unsupported version" error from `@okta/okta-react` the moment a route using `SecureRoute` renders. The cause is that `SecureRoute` only supports `react-router-dom` v5, and okta-react has thrown on v6 since version 6.4.3. The fix on React Router v6 or v7 is to drop `SecureRoute` and write a small `RequiredAuth` wrapper instead: call `useOktaAuth()`, null-check `authState`, redirect unauthenticated users with `oktaAuth.setOriginalUri(...)` plus `oktaAuth.signInWithRedirect()`, and render `<Outlet />` when the user is authenticated. This is the exact pattern in Okta's official v6 sample ([okta-react CHANGELOG](https://raw.githubusercontent.com/okta/okta-react/master/CHANGELOG.md), [okta-react README](https://github.com/okta/okta-react/blob/master/README.md)).

### `useNavigate() may be used only in the context of a <Router> component.`

The symptom is that exact runtime error on load, before any sign-in happens. The cause is that something calling `useNavigate()` is rendering outside the router. With okta-react this almost always means `<Security>` and its `restoreOriginalUri` callback (which calls `useNavigate()`) are mounted outside `<BrowserRouter>`. The fix is to make `<BrowserRouter>` the outer wrapper. In a Vite app, render `<BrowserRouter>` around `<App />` in `src/main.tsx`, and define `restoreOriginalUri` plus `<Security>` inside the `App` component so they run within the router context. Name that component `App`, not `AppWithRouterAccess` (the `AppWithRouterAccess` sample is the older v5 pattern that uses `useHistory()`).

### Sign-in fails with "User is not assigned to the client application" or `access_denied`

The symptom is that authentication itself succeeds but the user is bounced back with an error instead of a session. The cause is that the user (or a group they belong to) is not assigned to the Okta app integration. The fix is to assign them: open the app, go to the **Assignments** tab, and use **Assign → Assign to People** or **Assign to Groups**. How the error surfaces depends on the protocol. For OIDC, Okta redirects back with `error=access_denied` and `error_description=User is not assigned to the client application.`; `oktaAuth.handleLoginRedirect()` rejects, and `<LoginCallback>` renders its `errorComponent` with that `Error`. For SAML, Okta shows error `E0000004` and POSTs no assertion to your ACS at all, so handle a missing assertion as an auth failure ([Okta manage assignments](https://help.okta.com/oie/en-us/content/topics/apps/apps-manage-assignments.htm)).

### Signing out still silently signs the user back in

The symptom is that after "logging out," the next sign-in completes with no prompt, as if logout did nothing. This is expected behavior, not a bug. The cause is that a local logout only clears your app's own session (tokens or a session cookie); the user's Okta session is still live, so the next sign-in is satisfied silently by Okta. The fix depends on what you want. To end the app session only, that is already what is happening. To also end the Okta session, use `oktaAuth.signOut()` on the OIDC path (it revokes the app tokens and ends the Okta session), or SAML Single Logout on the SAML path (a separate, best-effort profile that signs the user out at Okta too) ([okta-auth-js README](https://github.com/okta/okta-auth-js/blob/master/README.md)).

### Okta groups claim missing from the ID token

The symptom is that `idToken.claims.groups` is `undefined` even though you added a groups claim in Okta. The usual cause is configuring the claim in the wrong place. If your issuer is the custom authorization server (`https://<your-org>.okta.com/oauth2/default`), the app's **Sign On** tab "Group claims filter" does not apply, because that field only affects the **Org** authorization server. The fix is to add the claim under **Security → API → Authorization Servers → `default` → Claims → Add Claim**. Two details matter: name the claim exactly `groups` (the claim key equals the Name verbatim and is case-sensitive, so `idToken.claims.groups` only resolves if you literally named it `groups`), and set "Include in" to a granted scope or to Any scope so the claim ships with your existing scopes ([Okta create claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/main/)).

### SAML: InvalidNameIDPolicy, audience mismatch, or clock skew

The symptom is that the assertion reaches your ACS but node-saml rejects it with a Name ID, audience, or timing error. There are three common causes, each with its own fix. `InvalidNameIDPolicy` means Okta's Name ID format does not match what your SP requested; align Okta's **Name ID format** (commonly `EmailAddress`) with your node-saml `identifierFormat`. An audience mismatch means the assertion's `<saml:Audience>` does not equal your SP Entity ID character-for-character; make Okta's **Audience URI** and node-saml `audience` byte-identical. Clock skew means `NotBefore` or `NotOnOrAfter` falls outside tolerance; sync the server clock with NTP and set a small `acceptedClockSkewMs` (single-digit seconds) rather than a large window ([ScaleKit: SAML debugging](https://www.scalekit.com/blog/saml-debugging-handbook-2026-how-to-diagnose-log-and-resolve-sso-failures), [WorkOS: common SAML errors](https://workos.com/blog/saml-assertion-failures-debugging-guide)).

### SAML: signature validation failures and certificate rotation

The symptom is that previously-working sign-ins start failing signature validation, often right after an Okta cert rotation. The cause is a stale pinned certificate: Okta keeps one IdP signing certificate at a time, so when it rotates, a hard-coded `idpCert` no longer matches the signature. The fix is to validate against current IdP metadata. Point your SP at Okta's **Metadata URL** (it auto-updates on rotation) instead of pasting a cert that you then have to redeploy. The other half of "signature problems" is an out-of-date library, so pin `@node-saml/node-saml` to `>=5.1.0` and confirm it pulls `xml-crypto` `>=6.1.2` (the patched version node-saml 5.1.0 ships); older versions carry critical signature-verification CVEs ([Okta: rotate the SAML signing certificate](https://support.okta.com/help/s/article/how-to-rotate-signing-certificate-for-custom-saml-applications?language=en_US), [CVE-2025-54419](https://nvd.nist.gov/vuln/detail/CVE-2025-54419)).

### SAML: `profile.groups` is a string, or the group check misbehaves

The symptom is a group check that works for users in several groups but breaks for a user in exactly one, sometimes by silently iterating over the characters of a string. The cause is that `@node-saml/node-saml` returns a multi-valued SAML attribute as a scalar string when there is one value and as an array when there are several. So `profile.groups` is a `string` for a single-group user and a `string[]` for a multi-group user, and calling `.includes()` or `.map()` on the scalar misbehaves. The fix is to normalize before you use it:

```typescript
const raw = profile.groups
const groups: string[] = Array.isArray(raw) ? raw : raw != null ? [String(raw)] : []
```

`Array.isArray()` doubles as the type guard here, since arbitrary SAML attributes come through node-saml's index signature typed as `unknown`. Group values are Okta group names, so renaming a group in Okta silently breaks any `groups.includes('Admins')` check ([node-saml README](https://github.com/node-saml/node-saml)).

### SAML: debugging the assertion with SAML-tracer

The symptom is that something in the assertion is wrong (Issuer, Audience, Recipient, or Conditions) but the raw SAML messages are base64-encoded in the network traffic and hard to read. The fix is the SAML-tracer browser extension for Firefox and Chrome, which decodes the base64 `SAMLRequest` and `SAMLResponse` inline so you can read the Issuer, Audience, Recipient, and `<saml:Conditions>` directly while you click through the flow. If you can't install an extension, the fallback is to open DevTools → Network, find the form POST to your ACS, copy the `SAMLResponse` form field, and base64-decode it by hand ([SAML-tracer](https://github.com/SimpleSAMLphp/SAML-tracer)).

### SAML: session cookie not sent to the SPA after sign-in

The symptom is that the assertion validates and the backend sets a session cookie, but the SPA's follow-up requests (like `/me`) come back unauthenticated. The cause is the cross-origin cookie trap: when the Vite SPA and the Express SP run on different origins, the browser treats the SP's cookie as third-party and won't attach it. The cleaner fix is to put both on one origin in development using the Vite dev proxy, forwarding the SP routes to the backend; the cookie is then first-party (`SameSite=Lax`, no dev TLS) and the post-ACS redirect is a clean relative path. If you must stay cross-origin, the cookie reaches the SPA only with `SameSite=None; Secure` (which forces HTTPS even in dev), CORS `Access-Control-Allow-Credentials: true` with an explicit, non-wildcard origin, and a client `fetch(..., { credentials: 'include' })` ([Vite server.proxy](https://vite.dev/config/server-options), [MDN: Set-Cookie SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie)).

For a single React SPA that you control, direct Okta OIDC remains simplest. Direct SAML works when required, but managed platforms become more attractive when multiple IdPs, mixed protocols, SCIM provisioning, or SAML maintenance enter the requirements.

## Frequently asked questions (FAQ)

## FAQ

### Is Okta the same as Auth0?

No. Okta acquired Auth0 in 2021, but they remain separate products with separate dashboards, SDKs, and pricing. Okta now brands them as two product lines — Auth0 as "Okta Customer Identity Cloud" and its workforce product as "Okta Workforce Identity Cloud" — though developers still see the Auth0 name in its dashboard, SDKs, and APIs. Okta is the workforce/enterprise identity platform; Auth0 is the developer-focused customer-identity platform. Pick one and follow its integration path; this walkthrough integrates Okta directly.

### When should a React app use Clerk instead of direct Okta SAML?

Use Clerk when the app needs enterprise Okta SSO without operating a SAML service provider, especially for B2B apps with Organizations, per-tenant routing, multiple IdPs, or SCIM provisioning. Direct Okta OIDC still fits one Okta org and browser-native OIDC.

## In this series

1. [Adding Okta SSO to a React app: SAML and OIDC walkthrough](https://clerk.com/articles/adding-okta-sso-to-a-react-app-saml-and-oidc-walkthrough.md)
2. [Adding Okta SSO to a React app: SAML and OIDC walkthrough - Part 2](https://clerk.com/articles/adding-okta-sso-to-a-react-app-saml-and-oidc-walkthrough-2.md)
3. **Adding Okta SSO to a React app: SAML and OIDC walkthrough - Part 3** (you are here)
