/** * TASK-0131 — SSO admin + discovery + enforce integration. * * Against the real API + Postgres + Redis (Testcontainers): the SSO provider * CRUD (create / read / rotate / delete), the reveal-once secret masking * (booleans only on read), the enforce toggle + policy MIRROR invariant, the * anonymous minimal-info login-page discovery (`/auth/sso/options`), and the * password-login ENFORCE gate (`403 {reason:"sso_required"}`). A per-run * self-signed X.509 (openssl) is the SAML signing cert. */ import { execFileSync } from "node:child_process"; import { generateKeyPairSync } from "node:crypto"; import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { createTestApp, type TestAppHandle } from "./helpers/api"; import { authHeaders, createUser } from "./helpers/seed"; let app: TestAppHandle; function selfSignedCert(): { certPem: string; keyPem: string } { const dir = mkdtempSync(join(tmpdir(), "sso-integ-")); const { privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); const keyPem = privateKey.export({ type: "pkcs8", format: "pem" }) as string; const keyPath = join(dir, "key.pem"); const certPath = join(dir, "cert.pem"); writeFileSync(keyPath, keyPem); execFileSync("openssl", ["req", "-x509", "-new", "-key", keyPath, "-subj", "/CN=integ-idp", "-days", "2", "-out", certPath]); return { certPem: readFileSync(certPath, "utf8"), keyPem }; } const json = async (res: Response): Promise> => (await res.json().catch(() => ({}))) as Record; describe("TASK-0131 SSO admin + discovery + enforce", () => { beforeAll(async () => { app = await createTestApp(); }, 180_000); afterAll(async () => { await app?.close(); }); it("read: no provider → { provider: null }; non-owner → 403", async () => { const owner = await createUser(app.request); const orgRes = await app.request("POST", "/api/v1/orgs", { headers: authHeaders(owner.accessToken!), body: { name: `SSO Org ${Date.now()}` } }); const org = (await json(orgRes)) as { id?: string; organization?: { id?: string } }; const orgId = org.id ?? org.organization?.id; expect(orgId).toBeTruthy(); const none = await app.request("GET", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); expect(none.status).toBe(200); expect((await json(none)).provider).toBeNull(); // A different user is not the owner. const other = await createUser(app.request); const forbidden = await app.request("GET", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(other.accessToken!) }); expect(forbidden.status).toBe(403); // Anonymous. const anon = await app.request("GET", `/api/v1/orgs/${orgId}/sso`); expect(anon.status).toBe(401); }); it("SAML lifecycle: create → masked read → rotate → enforce+mirror → delete clears", async () => { const owner = await createUser(app.request, { name: "SSO Owner" }); const orgRes = await app.request("POST", "/api/v1/orgs", { headers: authHeaders(owner.accessToken!), body: { name: `SSO Life ${Date.now()}` } }); const org = (await json(orgRes)) as { id?: string; organization?: { id?: string; slug?: string } }; const orgId = org.id ?? org.organization?.id; const slug = (org as { slug?: string }).slug ?? org.organization?.slug; const origin = process.env.WEB_ORIGIN ?? "http://localhost:3000"; const entity = `${origin}/saml/metadata/${slug}`; const { certPem } = selfSignedCert(); // CREATE (201) — the cert is the reveal-once plaintext. const created = await app.request("POST", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!), body: { type: "SAML", name: "Acme Okta", certificate: certPem, config: { entityId: entity, ssoUrl: "https://idp.acme.test/sso/saml", acsUrl: `${origin}/saml/acs`, nameIdAttribute: "nameId", emailAttribute: "mail" } } }); expect(created.status).toBe(201); const cbody = await json(created); expect((cbody.provider as { type: string; name: string }).type).toBe("SAML"); // DB invariant: the cert is NOT in config (plaintext never at rest there). const row = await app.prisma.ssoProvider.findUnique({ where: { organizationId: orgId! } }); expect(row).not.toBeNull(); expect(JSON.stringify(row?.config)).not.toContain("BEGIN CERTIFICATE"); expect(JSON.stringify(row?.config)).not.toContain(certPem); // READ — secrets masked to booleans (never the PEM itself). const read = await app.request("GET", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); expect(read.status).toBe(200); const rbody = await json(read); const prov = rbody.provider as { certificateConfigured: boolean; clientSecretConfigured: boolean | null; config: Record }; expect(prov.certificateConfigured).toBe(true); expect(JSON.stringify(prov)).not.toContain("BEGIN CERTIFICATE"); expect(prov.clientSecretConfigured).toBeFalsy(); // Duplicate create → 409. const dup = await app.request("POST", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!), body: { type: "SAML", name: "Dup", certificate: certPem, config: { entityId: entity, ssoUrl: "https://idp.acme.test/sso/saml", acsUrl: `${origin}/saml/acs` } } }); expect(dup.status).toBe(409); // ROTATE — a new cert moves the current one to previous. const { certPem: newCertPem } = selfSignedCert(); const rotated = await app.request("PATCH", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!), body: { certificate: newCertPem } }); expect(rotated.status).toBe(200); // Both the current + previous ciphertexts are present now (rotation grace). const orgRow = await app.prisma.organization.findUnique({ where: { id: orgId! } }); const ssoConfig = (orgRow?.ssoConfig as Record) ?? {}; expect(ssoConfig.certificateEncrypted).toBeTruthy(); expect(ssoConfig.previousCertificateEncrypted).toBeTruthy(); // ENFORCE → writes BOTH SsoProvider.enforce + policy.ssoEnforced (mirror). const enforce = await app.request("PATCH", `/api/v1/orgs/${orgId}/sso/enforce`, { headers: authHeaders(owner.accessToken!), body: { enforce: true } }); expect(enforce.status).toBe(200); expect((await json(enforce)).enforce).toBe(true); const afterEnforce = await app.prisma.organization.findUnique({ where: { id: orgId! }, select: { policy: true } }); expect((afterEnforce?.policy as { ssoEnforced?: boolean }).ssoEnforced).toBe(true); // ENFORCE GATE: the owner's PASSWORD login is now refused (the owner is a // member of an enforced org). const login = await app.request("POST", "/api/v1/auth/login", { body: { email: owner.email, password: owner.password } }); expect(login.status).toBe(403); const lbody = await json(login); expect((lbody.error as { reason?: string; details?: Array<{ reason?: string; orgs?: string[] }> })?.details?.[0]?.reason).toBe( "sso_required" ); expect((lbody.error as { details?: Array<{ orgs?: string[] }> }).details?.[0]?.orgs).toContain(slug); // DISCOVERY (anonymous, minimal-info): the owner's email resolves the // org slug + a protocol-agnostic start URL. const options = await app.request("GET", `/api/v1/auth/sso/options?email=${encodeURIComponent(owner.email)}`); expect(options.status).toBe(200); const obody = await json(options); const oOrgs = obody.orgs as Array<{ slug: string; ssoAvailable: boolean; startUrl: string }>; expect(oOrgs.some((o) => o.slug === slug && o.ssoAvailable && o.startUrl.includes(`/sso/start/${slug}`))).toBe(true); // minimal-info: never the provider type / org id. expect(JSON.stringify(obody)).not.toContain("SAML"); // Unknown email → empty (no enumeration). const unknown = await app.request("GET", `/api/v1/auth/sso/options?email=${encodeURIComponent("ghost@nowhere.test")}`); expect((await json(unknown)).orgs).toEqual([]); // DELETE — removes the provider + clears the mirror policy flag. const del = await app.request("DELETE", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); expect(del.status).toBe(200); expect((await json(del)).deleted).toBe(true); const gone = await app.prisma.ssoProvider.findUnique({ where: { organizationId: orgId! } }); expect(gone).toBeNull(); const afterDelete = await app.prisma.organization.findUnique({ where: { id: orgId! }, select: { policy: true } }); expect((afterDelete?.policy as { ssoEnforced?: boolean }).ssoEnforced).toBe(false); // Password login works again. const loginAgain = await app.request("POST", "/api/v1/auth/login", { body: { email: owner.email, password: owner.password } }); expect(loginAgain.status).toBe(200); // Read after delete → provider null. const readAfter = await app.request("GET", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); expect((await json(readAfter)).provider).toBeNull(); }); it("OIDC create: reveal-once client secret (encrypted, masked on read)", async () => { const owner = await createUser(app.request); const orgRes = await app.request("POST", "/api/v1/orgs", { headers: authHeaders(owner.accessToken!), body: { name: `SSO OIDC ${Date.now()}` } }); const org = (await json(orgRes)) as { id?: string; organization?: { id?: string } }; const orgId = org.id ?? org.organization?.id; const origin = process.env.WEB_ORIGIN ?? "http://localhost:3000"; const created = await app.request("POST", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!), body: { type: "OIDC", name: "Acme Okta OIDC", clientSecret: "super-secret-123", config: { issuer: "https://idp.acme.test", clientId: "factory-client", authorizationEndpoint: "https://idp.acme.test/authorize", tokenEndpoint: "https://idp.acme.test/token", redirectUri: `${origin}/sso/oidc/${orgId}/callback` } } }); expect(created.status).toBe(201); // The secret is NOT in config; it's encrypted on the org row. const row = await app.prisma.ssoProvider.findUnique({ where: { organizationId: orgId! } }); expect(JSON.stringify(row?.config)).not.toContain("super-secret-123"); const orgRow = await app.prisma.organization.findUnique({ where: { id: orgId! } }); expect(((orgRow?.ssoConfig as Record) ?? {}).clientSecretEncrypted).toBeTruthy(); // Read masks the secret to a boolean. const read = await app.request("GET", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); const prov = (await json(read)).provider as { clientSecretConfigured: boolean; certificateConfigured: boolean | null }; expect(prov.clientSecretConfigured).toBe(true); expect(prov.certificateConfigured).toBeFalsy(); await app.request("DELETE", `/api/v1/orgs/${orgId}/sso`, { headers: authHeaders(owner.accessToken!) }); }); });