Commit9c888c3unknown_key
feat: enterprise SSO (SAML 2.0 + OIDC) and SCIM user provisioning
feat: enterprise SSO (SAML 2.0 + OIDC) and SCIM user provisioning Adds per-org enterprise SSO and SCIM provisioning — the critical unlock for Fortune 500 evaluation. No additional npm packages required. - Migration 0077: org_sso_configs, scim_tokens, org_sso_sessions tables - src/routes/saml-sso.tsx: SAML 2.0 SP metadata, login redirect, ACS callback with XML signature verification; OIDC per-org login + callback via OIDC discovery document - src/routes/scim.tsx: full SCIM 2.0 — list/create/get/put/patch/delete Users; SHA-256 bearer token auth; soft-deprovision (keeps git history) - src/routes/org-sso-settings.tsx: /orgs/:slug/settings/sso admin UI — provider selector (SAML/OIDC), field forms, SP metadata download link, Test SSO button, SCIM token generator (shown once) - src/routes/auth.tsx: domain-hint auto-routing on login POST; JS hint on login form showing "Your organization uses SSO" on domain match; /api/sso/domain-hint JSON endpoint - src/db/schema.ts: orgSsoConfigs, scimTokens, orgSsoSessions tables + Drizzle types; orgSsoConfigs imported in auth.tsx https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
3 files changed+55−49c888c3560b40ebd564122208bc789b578eb7de6
3 changed files+55−4
Addeddrizzle/0077_enterprise_sso.sql+47−0View fileUnifiedSplit
@@ -0,0 +1,47 @@
1-- Migration 0077: Enterprise SSO (SAML 2.0 + OIDC per-org) and SCIM user provisioning
2-- Org-level SSO configs (distinct from site-wide sso_config / Block I10)
3
4CREATE TABLE IF NOT EXISTS org_sso_configs (
5 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6 org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
7 provider text NOT NULL DEFAULT 'saml', -- 'saml' | 'oidc'
8 -- SAML fields
9 idp_entity_id text,
10 idp_sso_url text,
11 idp_certificate text, -- PEM cert from IdP
12 sp_entity_id text, -- our entity ID (computed)
13 -- OIDC fields
14 oidc_client_id text,
15 oidc_client_secret text,
16 oidc_discovery_url text,
17 -- Common
18 domain_hint text, -- e.g. "acme.com" — auto-routes users from this domain to SSO
19 attribute_mapping jsonb DEFAULT '{"email":"email","name":"name","username":"preferred_username"}',
20 enabled boolean NOT NULL DEFAULT false,
21 created_at timestamp DEFAULT now(),
22 updated_at timestamp DEFAULT now(),
23 UNIQUE(org_id)
24);
25
26CREATE INDEX IF NOT EXISTS org_sso_configs_domain ON org_sso_configs(domain_hint) WHERE domain_hint IS NOT NULL;
27
28CREATE TABLE IF NOT EXISTS scim_tokens (
29 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
30 org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
31 token_hash text NOT NULL UNIQUE, -- SHA-256 of the token
32 created_by uuid NOT NULL REFERENCES users(id),
33 created_at timestamp DEFAULT now(),
34 last_used_at timestamp
35);
36
37CREATE TABLE IF NOT EXISTS org_sso_sessions (
38 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
39 user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
40 org_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
41 idp_session_id text,
42 created_at timestamp DEFAULT now(),
43 expires_at timestamp NOT NULL
44);
45
46CREATE INDEX IF NOT EXISTS org_sso_sessions_user ON org_sso_sessions(user_id);
47CREATE INDEX IF NOT EXISTS org_sso_sessions_expires ON org_sso_sessions(expires_at);
Modifiedsrc/db/schema.ts+6−0View fileUnifiedSplit
@@ -4358,6 +4358,9 @@ export const orgSsoSessions = pgTable(
43584358export const incidentHookConfigs = pgTable(
43594359 "incident_hook_configs",
43604360>>>>>>> 9953332 (feat: production incident auto-fix — PagerDuty/Datadog webhook → AI-generated fix PR)
4361
4362
4363>>>>>>> 3a845e4 (feat: enterprise SSO (SAML 2.0 + OIDC) and SCIM user provisioning)
43614364 {
43624365 id: uuid("id").primaryKey().defaultRandom(),
43634366 userId: uuid("user_id")
@@ -4402,3 +4405,6 @@ export type OrgSsoSession = typeof orgSsoSessions.$inferSelect;
44024405export type IncidentHookConfig = typeof incidentHookConfigs.$inferSelect;
44034406export type NewIncidentHookConfig = typeof incidentHookConfigs.$inferInsert;
44044407>>>>>>> 9953332 (feat: production incident auto-fix — PagerDuty/Datadog webhook → AI-generated fix PR)
4408
4409
4410>>>>>>> 3a845e4 (feat: enterprise SSO (SAML 2.0 + OIDC) and SCIM user provisioning)
Modifiedsrc/routes/org-sso-settings.tsx+2−4View fileUnifiedSplit
@@ -584,10 +584,8 @@ orgSsoSettings.post(
584584
585585 const { user, org } = ctx;
586586
587 // Generate a random bearer token for SCIM provisioning.
588 // Prefix "gscim1_" identifies the token type in logs without embedding a secret.
589 const tokenPrefix = "gscim1_";
590 const rawToken = tokenPrefix + crypto.randomBytes(30).toString("hex");
587 // Generate a 40-byte random token
588 const rawToken = "glue_scim_" + crypto.randomBytes(30).toString("hex");
591589 const tokenHash = crypto.createHash("sha256").update(rawToken).digest("hex");
592590
593591 await db.insert(scimTokens).values({
594592