Docs menu
SSO (OAuth/OIDC/SAML/JWT)
An SSO provider that resolves a request coming from an enterprise identity provider (IdP/gateway) into a Principal; runs before userStore and fallback in createEnterpriseAuth's identity chain.
What it's for / when to use it#
In enterprise deployments, authentication is usually already done outside GNL, by an IdP/gateway (Okta, Azure AD, Auth0, an API gateway...), and by the time the request reaches GNL it carries a signed JWT. createJwtSso is exactly for this scenario: it doesn't run a redirect/login flow — it verifies the JWT in the incoming request's Authorization: Bearer <jwt> (or a custom header) for signature + expiry (exp) + iss/aud, and derives a Principal from its claims.
Besides that, two named providers run the real redirect+token flow end to end: createAuth0Sso (OIDC authorization-code — exchanges the code for an id_token at /oauth/token, verifies the id_token via verifyJwt with an RS256 public key derived from Auth0's JWKS; iss/aud are required) and createWorkOsSso (WorkOS SSO API — exchanges the code for a profile at /sso/token). Today there are three real paths usable in production: createJwtSso (a JWT already verified upstream), createAuth0Sso, and createWorkOsSso.
Setup / import#
import { createJwtSso, createAuth0Sso, createWorkOsSso, createEnterpriseAuth } from '@gnldev/auth-ee';@gnldev/auth-ee is a paid (Enterprise) package — without a valid license key, createEnterpriseAuth automatically falls back to fallback (see the warning below). The SsoProvider interface doesn't extend the free @gnldev/auth's AuthProvider contract — it's only passed to createEnterpriseAuth as the sso option.
Step-by-step usage#
1. Give createJwtSso the verification parameters — secret for HS256, publicKey (PEM or base64url DER/spki) for RS256/Ed25519; one of the two is required, and the factory throws if neither is given:
const sso = createJwtSso({
publicKey: process.env.SSO_JWT_PUBLIC_KEY, // the public key taken from Okta / Azure AD / Auth0 JWKS
issuer: 'https://idp.example.com',
audience: 'gnl-api',
// claimMap: { id: 'sub', roles: 'roles', orgId: 'orgId' } (default)
});2. Pass sso to createEnterpriseAuth — in the identity chain, SSO is tried first, then journal-backed userStore, and finally the free fallback:
const auth = createEnterpriseAuth({
licenseKey: process.env.GNL_LICENSE_KEY,
sso,
fallback: roleAuth({ admin: { token: process.env.GNL_ADMIN_TOKEN } }),
});
app.route('/api', createRestApi(config, { title: 'GNL API', auth }));3. The client sends the JWT it got from the IdP as usual; createJwtSso resolves the request directly via principalFromRequest(c) (this is called automatically inside createEnterpriseAuth's authenticate step — there's nothing extra to do in host code):
curl -s http://localhost:3001/api/agents \
-H 'authorization: Bearer <jwt-signed-by-your-idp>'If the JWT is invalid, expired, or malformed, principalFromRequest never throws — it silently returns null and the chain falls through to the next step (userStore, then fallback).
4. createAuth0Sso — give it your Auth0 tenant's domain/clientId/clientSecret/redirectUri; authorizeUrl redirects to /authorize, handleCallback exchanges the code for an id_token at /oauth/token and verifies it with RS256 against Auth0's JWKS (.well-known/jwks.json) (iss/aud required):
const auth0 = createAuth0Sso({
domain: 'acme.eu.auth0.com',
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
redirectUri: 'https://app.example.com/callback',
validateState: (state) => checkStateAgainstSession(state), // CSRF — optional but recommended
});
const auth = createEnterpriseAuth({ licenseKey, sso: auth0, fallback });The JWKS network call is cached in-memory for a jwksTtlMs (default 10 minutes) — since principalFromRequest runs on every request, without this cache every request would be a JWKS fetch; if the fetch fails while the cache is stale (an Auth0 outage), a stale copy keeps being used if one is available (stale-while-error).
5. createWorkOsSso — uses the WorkOS SSO API; handleCallback exchanges the code for a profile at /sso/token and derives a Principal (the WorkOS profile already arrives signed/verified by WorkOS itself — no separate JWKS verification is needed):
const workos = createWorkOsSso({
clientId: process.env.WORKOS_CLIENT_ID!,
apiKey: process.env.WORKOS_API_KEY!,
redirectUri: 'https://app.example.com/callback',
validateState: (state) => checkStateAgainstSession(state),
});createWorkOsSso ONLY sets up the handleCallback-based login flow — WorkOS has no verifiable bearer-token contract carried on the request, so principalFromRequest always returns null. If you wire this up alone as createEnterpriseAuth({ sso: workos }), the SSO step returns null on EVERY request and protected endpoints silently fall through to anonymous — usually not the desired behavior. The correct pattern: combine the Principal returned from the WorkOS callback with your own session JWT, signed and merged via createJwtSso:const session = createJwtSso({ secret: process.env.SESSION_SECRET!, issuer: 'my-app' });
// callback ucunda:
const principal = await workos.handleCallback(c);
const jwt = signMySessionJwt(principal); // a short-lived session JWT you sign yourself
// hand it to the client via Set-Cookie or the body
// sonraki istekler:
const auth = createEnterpriseAuth({ licenseKey, sso: session, fallback });API reference#
createJwtSsoJwtSsoOptions → SsoProvider. Verifies a JWT coming from upstream with signature+exp+iss/aud, derives a Principal from its claims (doesn't run a redirect/login flow).
createAuth0SsoAuth0SsoOptions → SsoProvider. OIDC authorization-code: code→id_token exchange + RS256 verification with Auth0's JWKS (iss/aud required), TTL-cached JWKS, an optional validateState CSRF hook.
Auth0SsoOptions{ domain, clientId, clientSecret, redirectUri, scope?, claimMap?, validateState?, fetch?, jwks?, jwksTtlMs?, now? } — createAuth0Sso's configuration.
createWorkOsSsoWorkOsSsoOptions → SsoProvider. WorkOS SSO API: derives a Principal via a code→profile exchange. principalFromRequest ALWAYS returns null — it only sets up the login flow, and must be combined with createJwtSso.
WorkOsSsoOptions{ clientId, apiKey, redirectUri, organization?, connection?, validateState?, fetch? } — createWorkOsSso's configuration.
SsoProvider{ authorizeUrl(state), handleCallback(c), principalFromRequest(c) } — createEnterpriseAuth's sso option expects this contract.
JwtSsoOptionssecret (HS256) or publicKey (RS256/Ed25519, PEM or base64url DER/spki), issuer?, audience?, claimMap?, header? — createJwtSso's configuration.
@gnldev/auth-ee is a paid layer: without a valid licenseKey, createEnterpriseAuth logs a warning and falls back to fallback (sso never kicks in); with failClosed: true, boot can be made to throw instead.alg in the JWT header: the expected algorithm is derived from the secret/publicKey key type (HS256 / RS256 / EdDSA) and cross-checked against the header's value. Also, tokens with no exp claim or that are expired are always rejected.sso → userStore → fallback. This way, even while SSO is active, you can sign in with an operator bootstrap token against an empty user store and create the first users.