DEV Community

Sandro Munda for RootCX

Posted on • Originally published at rootcx.com

How to Add SSO to Your AI-Coded Internal App (OIDC Guide)

You built an internal app with Claude Code or Cursor. It works. The logic is solid. Your team wants to use it tomorrow.

Then your CTO asks: "How do our people log in with their Okta credentials?"

And suddenly you are spending the next 2 weeks not shipping the tool your team needs, but wrestling with OAuth flows, token validation, session management, and edge cases you did not know existed.

This guide will get you from zero to production SSO. You will understand how OIDC actually works, see a full implementation you can copy, learn where the real complexity hides, and choose the approach that fits your situation. Or, if you just want SSO working in 10 minutes, skip straight to the easy path.

What you will learn:

  • How OIDC works (Authorization Code flow, tokens, claims, PKCE)

  • A full working SSO implementation you can copy into any Node.js app

  • The production complexity most tutorials skip (session revocation, offboarding, role mapping)

  • How to choose between building from scratch, Auth.js, managed providers, or a platform like RootCX

  • A pre-launch checklist for production-ready SSO

Why your internal app needs SSO before it ships

Your team already has an identity provider. Okta, Microsoft Entra, Google Workspace, Auth0. Every internal app should authenticate against it.

Not "should eventually." Should now. Here is why:

The compliance clock is ticking. SOC 2, ISO 27001, and HIPAA all require centralized identity management. Every internal tool with its own password is a finding on your next audit. The longer you wait, the more tools accumulate without it.

The offboarding gap is a security hole. When someone leaves, IT disables their IdP account. Done. But if your internal app has its own login? That person still has access. For days. Sometimes months. Until someone remembers to check.

Perception shapes adoption. The moment a compliance officer sees your internal tools authenticate through SSO, the conversation shifts. You stop being "that thing the engineering team built" and start being production software.

How OIDC works: the protocol behind SSO

OpenID Connect is the protocol that powers SSO in every modern app. Built on OAuth 2.0, it adds an identity layer: not just "can this app access my resources" but "who is this person."

Worth 5 minutes of your time to understand. Everything else builds on this.

OIDC vs SAML: which one?

OIDC
SAML 2.0

Format
JSON + JWT
XML + X.509 signatures

Token size
~1 KB
5-20 KB

Best for
Modern apps, SPAs, mobile
Legacy enterprise (Workday, SAP, on-prem ADFS)

Complexity
Manageable
XML canonicalization, signature wrapping attacks

Use OIDC. Only reach for SAML if a customer's IdP literally cannot speak OIDC (increasingly rare in 2026).

The Authorization Code flow, step by step

When a user clicks "Sign in with Okta," here is what actually happens:

  • Your app redirects to the IdP. It sends your client ID, the scopes you want (openid email profile), a redirect URI, a random state value, and a PKCE code challenge.

  • The IdP handles authentication. Login page, MFA, session check. Your app never sees their password.

  • The IdP redirects back with a code. A short-lived, single-use authorization code in the URL. Useless without your client secret.

  • Your server exchanges the code for tokens. Server-to-server call. Sends the code + client secret + PKCE verifier. The browser never sees this.

  • You get tokens back. An ID token (signed JWT proving identity), an access token, and optionally a refresh token.

  • You validate the ID token. Check JWT signature against the IdP's public keys. Verify issuer, audience, expiry.

  • Create a session. The user is authenticated.

That is the happy path. 7 steps. Sounds manageable. Let's implement it.

Concepts you will encounter

ID token vs access token. The ID token is for YOUR app. It says "this person is X, authenticated at time T." The access token is for calling APIs (like the IdP's /userinfo). Do not use the access token to verify identity.

Claims. Key-value pairs inside the JWT: sub (stable user ID), email, name, iss (issuer), aud (audience), exp (expiry). These are your source of truth about the user.

Scopes. What you ask for. openid is required (otherwise it is just OAuth, not OIDC). email gets their email. profile gets their name. offline_access gets a refresh token.

Discovery. Every OIDC provider publishes its configuration at {issuer}/.well-known/openid-configuration. All endpoints, supported scopes, signing algorithms. Your app can auto-configure for any provider from just the issuer URL.

How to implement OIDC SSO in Node.js (Next.js, Express, Hono)

Whatever you built your app with (Next.js, Express, SvelteKit, Hono), the OIDC flow is identical. The code below uses openid-client v6. It is framework-agnostic and works anywhere you have Node.js running.

npm install openid-client
Enter fullscreen mode Exit fullscreen mode

The core implementation

import * as client from "openid-client";

// Discovery: auto-configure from the issuer URL
const config = await client.discovery(
  new URL(process.env.OIDC_ISSUER!), // https://your-org.okta.com
  process.env.OIDC_CLIENT_ID!,
  process.env.OIDC_CLIENT_SECRET!
);

// Build the login redirect
async function buildLoginUrl(redirectUri: string) {
  const codeVerifier = client.randomPKCECodeVerifier();
  const state = client.randomState();
  const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);

  const authUrl = client.buildAuthorizationUrl(config, {
    redirect_uri: redirectUri,
    scope: "openid email profile",
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    state,
  });

  // Store codeVerifier + state in your session (cookie, DB, Redis)
  return { url: authUrl.href, codeVerifier, state };
}

// Handle the callback after the IdP redirects back
async function handleCallback(
  callbackUrl: URL,
  codeVerifier: string,
  expectedState: string
) {
  const tokens = await client.authorizationCodeGrant(config, callbackUrl, {
    pkceCodeVerifier: codeVerifier,
    expectedState,
  });

  // openid-client validates automatically:
  // JWT signature, issuer, audience, expiry
  const claims = tokens.claims();

  return {
    sub: claims.sub,
    email: claims.email,
    name: claims.name,
    refreshToken: tokens.refresh_token,
  };
}

// Refresh tokens when access expires
async function refresh(refreshToken: string) {
  return await client.refreshTokenGrant(config, refreshToken);
}
Enter fullscreen mode Exit fullscreen mode

Plug it into your framework

Next.js App Router: Redirect in app/auth/login/route.ts. Store codeVerifier + state in an encrypted cookie. Handle callback in app/auth/callback/route.ts.

Next.js Pages Router: Same logic in pages/api/auth/login.ts and pages/api/auth/callback.ts.

Express / Hono / Fastify: Redirect on GET /auth/login, session middleware stores codeVerifier, callback on GET /auth/callback.

Using Claude Code? Prompt it: "Add OIDC SSO with Okta to my Next.js app using openid-client." It will scaffold the route handlers and basic session management. The happy path will work. What it will not generate for you: everything in the next section.

This works. For a single provider, a single app, with happy-path users.

If that is all you need, you are done. Copy the code, wire it in, ship it.

Don't want to maintain this for 5 apps? RootCX includes SSO in the production stack. Configure once, every app inherits it. But keep reading either way. You should understand what production SSO actually demands.

SSO in production: session management, RBAC, and offboarding

The code above handles about 30% of production SSO. Here is the other 70%.

Session revocation

You stored the session in a JWT cookie. Great. Now someone gets fired. Their Okta account is disabled immediately.

Your app? Still works for them. For the entire JWT lifetime.

The fix: server-side sessions (PostgreSQL or Redis) with a short TTL (15-30 minutes). Refresh via the IdP token endpoint. When the refresh fails, the session dies. This catches offboarding within minutes instead of hours.

But now you are building session infrastructure. Connection pooling. Cleanup jobs. One more thing to monitor.

In RootCX: server-side sessions in PostgreSQL, 15-minute access tokens, automatic refresh. Disable a user in your IdP, their session dies within minutes. Zero session code to write.

Multiple identity providers

First customer uses Okta. Second uses Azure AD. Third uses Google Workspace.

Now you need:

  • Provider configs stored in a database (not env vars)

  • Per-provider callback routing

  • Email deduplication (same person in both Okta and Google)

  • Domain-to-provider mapping (@acme.com goes to Okta, @startup.io uses Google)

Each provider sends claims slightly differently. Each has its own quirks. This is a week of work at minimum.

Role mapping

Okta sends groups: ["Engineering", "Admin"]. Azure AD sends roles: ["App.ReadWrite"]. Google Workspace sends nothing about groups by default (you need the Directory API).

You need a mapping layer. "Okta group 'Engineering' = app role 'editor'." And you need an admin UI so someone can configure this without deploying code. This alone is a week of work. (For a deep dive, see RBAC for Internal Tools: the Complete Guide.)

In RootCX: role-based permissions with namespaces, wildcards, and inheritance are built into the platform. Define a role once, it applies across every app and every agent on the project. See how RBAC works.

Offboarding (the hard problem)

When someone is disabled in the IdP, how does your app find out?

Option A: Wait for their session to expire and refresh to fail. (Gap: minutes to hours.)
Option B: Implement SCIM. (A full REST API the IdP calls to create/update/delete users in your app. Weeks of work.)
Option C: Back-channel logout. (The IdP POSTs to your app when a session is revoked. Requires a public endpoint and correct token validation.)

Without B or C, your offboarding SLA is "whenever the session expires." Enterprise security teams will flag this.

In RootCX: offboarding is automatic. Sessions are killed on failed token refresh. No SCIM to implement, no back-channel logout endpoint to build. Disable the user in your IdP, RootCX catches it within minutes.

Account linking

A user signed up with email + password 3 months ago. Today they click "Sign in with Google" using the same email. What happens?

If you auto-link: account takeover risk. An attacker registers a Google account with someone else's corporate email and gains access.

If you reject: frustrated user who cannot log in.

The correct answer: only auto-link if email_verified is true on BOTH sides. Otherwise, require the user to authenticate with their existing method first, then link manually.

Edge cases that will find you

  • IdP-initiated login. User clicks your app in their Okta dashboard. They arrive at your callback without you initiating the request. No state to validate. Enterprise customers expect this to work.

  • Email domain verification. You route @acme.com to Acme's Okta. But have you verified Acme actually owns that domain? Without a DNS TXT check, anyone can claim it.

  • Invited-only vs open. Should every authenticated Okta user get an account? Or only people who were explicitly invited?

SSO solutions compared: build vs buy vs platform

Option 1: Build from scratch

Use openid-client or raw HTTP. Own every line.

Timeline: 2-4 weeks for a single provider. 6-10 weeks to add SCIM, multi-provider, role mapping, back-channel logout, and an admin UI.

True cost: Engineering time. And ongoing maintenance every time a provider changes their implementation or a new edge case appears.

Best for: Products where auth IS the product. Teams with dedicated security engineers.

Option 2: Auth libraries (Auth.js, Arctic)

Auth.js (formerly NextAuth.js) handles the OAuth dance for 50+ providers. Free and open source.

But you still build: multi-tenant provider routing, role mapping, SCIM, offboarding logic, admin portal. The library gives you the handshake. Everything else is on you.

Timeline: 1-3 weeks for basic SSO. Weeks more for enterprise requirements.

Best for: SaaS apps that need customer-facing SSO and want full UX control.

Option 3: Managed auth providers

Provider
Cost (10 SSO connections)
What you get

WorkOS
~$1,250/mo
SSO + SCIM + Admin Portal

Auth0
~$1,000-2,800/mo
Full auth + SSO + SCIM

Clerk
~$695/mo
Full auth + SSO (SCIM on higher plans)

Fastest to integrate. Days, not weeks. But per-connection pricing scales linearly. And you are still wiring it into your app's permission model, session lifecycle, and user management.

Best for: SaaS products selling to enterprise customers who each need their own IdP.

Option 4: Ship on infrastructure that already has SSO

If you are building internal tools (not a SaaS product), step back for a second.

You do not need per-customer IdP management. You do not need SCIM. You do not need an admin portal. You need one SSO connection for your company. And you probably need it for more than one app.

So why are you building auth infrastructure at all?

RootCX includes SSO as part of the production stack. You configure your OIDC provider once:

ROOTCX_OIDC_ISSUER=https://your-org.okta.com
ROOTCX_OIDC_CLIENT_ID=your-client-id
ROOTCX_OIDC_CLIENT_SECRET=your-client-secret
Enter fullscreen mode Exit fullscreen mode

That is it. 3 environment variables. No code. No library. No session store to maintain.

Here is what you get out of the box:

  • SSO with any OIDC provider. Okta, Microsoft Entra, Google Workspace, Auth0. Configure once, every app inherits it.

  • Session revocation that actually works. Server-side sessions in PostgreSQL. 15-minute access tokens. Disable the user in your IdP, they are locked out within minutes.

  • Role-based permissions. Map IdP groups to app roles. Define who can view, edit, delete, on every resource. One permission model across all your apps.

  • Immutable audit logs. Every authentication event, every action (human or AI agent), logged at the database trigger level. Not application-level logging that can be bypassed.

  • Offboarding handled. No SCIM to implement. The platform catches disabled users on token refresh and kills their sessions.

  • AI agents under the same rules. If you are building AI agents alongside internal apps, they authenticate and operate under the same RBAC. Same audit trail. Same security model.

The part that matters most: the next internal app you build gets all of this automatically. No auth code. No integration work. You build the business logic, SSO is already there. (See also: How to Deploy Your AI-Coded Internal App for the full deploy workflow.)

SSO is included on every plan, including free. No credit card required.

Start your project on RootCX and add SSO in under 10 minutes.

Best for: Teams shipping internal tools and AI agents that need production-grade auth today, not in 6 weeks.

How to choose

Your situation
Go with

Building a SaaS with per-customer SSO
WorkOS or Auth0

1 internal app, you enjoy auth work
Build from scratch

Multiple internal apps for your team
RootCX (SSO included, free tier)

Need it working by end of week
RootCX or managed provider

Pre-launch checklist

Before you ship, regardless of which path you chose:

  • Access tokens expire in 15 minutes or less

  • Refresh tokens rotate on each use

  • Sessions are server-side and revocable (not JWT-only)

  • Account linking requires email_verified: true on both sides

  • Disabling a user in the IdP kills their session within minutes

  • State and PKCE validated on every callback

  • You have an audit trail: who authenticated, when, from where

  • Graceful behavior when the IdP is temporarily unreachable

  • Tested against a real IdP, not just a localhost mock

On RootCX, every item on this checklist is handled by the platform. On other approaches, each one is something you build and maintain.

FAQ

How long does it take to add SSO to an internal app?

It depends on your approach. Building OIDC from scratch with a single provider takes 2-4 weeks. Adding multi-provider support, RBAC, SCIM, and session management pushes it to 6-10 weeks. Using a managed provider (Auth0, WorkOS, Clerk) cuts it to days. On a platform like RootCX that includes SSO in the infrastructure, it takes under 10 minutes: 3 environment variables and every app inherits authentication.

Should I use OIDC or SAML for SSO?

Use OIDC. It is simpler (JSON + JWT vs XML), lighter (1 KB tokens vs 5-20 KB), and supported by every modern identity provider (Okta, Microsoft Entra, Google Workspace, Auth0). Only use SAML if an enterprise customer's IdP cannot speak OIDC, which is increasingly rare in 2026.

Is SSO free or does it require an enterprise plan?

It depends on the tool. Many platforms (Retool, for example) lock SSO behind expensive enterprise tiers ($50/user/month). Auth0 and Clerk require paid plans for enterprise SSO connections. WorkOS charges $125/connection/month. RootCX includes SSO on every plan, including the free tier, with no per-connection pricing.

What is the difference between SSO and OAuth?

OAuth 2.0 is an authorization protocol. It answers "can this app access my resources?" but says nothing about who the user is. OIDC (OpenID Connect) is built on top of OAuth and adds an identity layer: it proves who the user is via a signed ID token (JWT). When people say "SSO," they almost always mean OIDC.

Do I need SCIM if I have SSO?

Not necessarily. SCIM handles user provisioning and deprovisioning (creating/deleting accounts in your app when the IdP changes). Without SCIM, offboarding depends on session expiry or token refresh failure. For internal tools, server-side sessions with short TTLs (15 minutes) catch disabled users quickly enough for most security requirements without implementing SCIM.

Can AI agents use SSO?

Yes, but most platforms do not support this. On RootCX, AI agents authenticate and operate under the same RBAC as human users. They inherit the SSO-based identity layer, follow the same permission rules, and every action they take is logged in the same audit trail. This means you can control what an agent can and cannot do using the same role system you use for your team.

SSO is the work that separates "it runs on my laptop" from "the team uses it in production." The protocol is well-documented. The happy path is straightforward. The edge cases are where you lose weeks.

If you are building a SaaS product, invest in the auth infrastructure. It is part of your product.

If you are building internal tools, stop rebuilding it. Ship on a stack that already has it.

Related reading:

Top comments (0)