Wiring a non-native IdP
CNS's ssojwt identity adapter is IdP-shape-agnostic by design (see
Authentication) — it never hardcodes any one
provider. This guide works through a concrete, verified example: a tenant whose users
authenticate via Supabase Auth instead of a central SSO, based on a real worked
recipe from this project's own deployment history.
The gap this closes
A Supabase-issued access token is a normal JWT, but it differs from a typical
central-SSO token in two ways ssojwt needed small, additive config knobs for:
- No tenant claim. A default Supabase access token carries no CNS-native tenant claim at all.
- A different phone claim name. Supabase's claim is
phone;ssojwt's default claim-name convention expectsphone_number.
Everything else — sub (Supabase's auth.users UUID) and email — already lines up
with what identity-mapping needs: it
resolves/stitches purely off email/phone, never off the raw sub, so a Supabase
user's UUID never needs to match anything on the CNS side.
Config recipe
cns-server picks a signing-key source the same way for any IdP — a static HMAC
secret, or a JWKS URL. Supabase supports both:
- JWKS (recommended for a rotatable setup):
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.jsononce the project is on asymmetric (ES256/RS256) signing keys. - HS256 shared secret (legacy): every Supabase project has one under
Project Settings → API → JWT Settings — set as
CNS_SSO_JWT_HMAC_SECRETif the project hasn't migrated to JWKS-based signing.
# --- pick ONE signing-key source ---
# JWKS (preferred):
export CNS_SSO_JWT_JWKS_URL="https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json"
# OR HS256 (a deployment-time secret from the Supabase project's dashboard —
# never hardcode or commit it):
export CNS_SSO_JWT_HMAC_SECRET="<the project's JWT secret>"
# --- common to either signing-key source ---
export CNS_SSO_JWT_ISSUER="https://<project-ref>.supabase.co/auth/v1"
export CNS_SSO_JWT_AUDIENCE="authenticated" # Supabase's standard aud claim
# Gap 1: no tenant claim — pin this deployment's one tenant (Phase-1 is
# single-tenant per process anyway). Match CNS_DEFAULT_TENANT_ID unless this
# tenant deliberately runs as its own cns-server process with a different id.
export CNS_SSO_JWT_STATIC_TENANT_ID="acme"
export CNS_DEFAULT_TENANT_ID="acme"
# Gap 2: Supabase's phone claim is "phone", not ssojwt's default "phone_number".
# (Email claim name is already "email" on both sides — no override needed.)
export CNS_SSO_JWT_CLAIM_PHONE="phone"
internal/config.Load reads these directly — there's no file-based config in Phase
- Every one of these four knobs (
CNS_SSO_JWT_STATIC_TENANT_ID,CNS_SSO_JWT_CLAIM_TENANT_ID,CNS_SSO_JWT_CLAIM_EMAIL,CNS_SSO_JWT_CLAIM_PHONE) defaults to unset — meaning "behave exactly like the default SSO-JWT shape" — so adding this recipe for one tenant does not change how any other, natively-configured tenant authenticates.
What this recipe does not decide
- Which concrete IdP project/URL/secret a given tenant uses in production is a deployment-time value you own — never hardcode it into the codebase.
- Whether this tenant shares a
cns-serverprocess with a natively-SSO'd tenant. Phase 1 loads exactly one globalProfile— and therefore one Identity-adapter config — per process (see Adapters & Profiles). If one tenant needs to authenticate via Supabase while another tenant's users keep authenticating via the platform's own SSO at the same time, that's two different Identity-adapter configs, and therefore two differentcns-serverprocesses today — not a single-process multi-IdP feature. Per-tenant profile keying (Phase 2) is what removes this constraint; see Advanced & Roadmap.
Verifying the recipe
internal/adapters/identity/ssojwt's test suite includes a case built against
exactly this claim shape (a sub UUID, email, phone, no tenant_id) verifying
the static-tenant-fallback path end to end — if you're adapting this recipe for a
different non-native IdP, that test is a good template for confirming your own claim
mapping resolves the way you expect before pointing real traffic at it.