sc-idp/README.md
2026-06-01 16:40:54 -05:00

7.9 KiB

saltcorn-idp

A Saltcorn plugin that turns a Saltcorn instance into a single sign-on (SSO) Identity Provider. It speaks three protocols against one shared identity model:

  • OIDC / OAuth2 (authorization-code + PKCE) via oidc-provider
  • LDAP with groups (LDAPS) via ldapjs
  • SAML 2.0 (SP-initiated, IdP-initiated, Single Logout) via samlify

All three are multi-tenant aware: each Saltcorn tenant gets its own issuer, asymmetric signing key, and SAML certificate, with the per-tenant private key sealed at rest (AES-256-GCM) under a key derived from SALTCORN_SESSION_SECRET.

  • Package name: saltcorn-idp
  • Version: 0.0.1
  • License: MIT
  • Node: >=20
  • Saltcorn plugin API version: 1

The plugin integrates through the Saltcorn routes export plus an onLoad hook (it does not register a Saltcorn authentication method). See index.js and lib/routes.js.


Feature / status matrix

All three protocol surfaces are built and gated by end-to-end tests. Remaining work is hardening, not new protocols.

Protocol Library Public base path Status
OIDC / OAuth2 (auth-code + PKCE) oidc-provider ^9.8.4 /idp Built; covered by test/e2e.js and test/mtGate.js
LDAP with groups (LDAPS only) ldapjs 3.0.7 LDAPS listener (opt-in via env) Built; covered by test/ldapGate.js and test/ldapMultiTenantGate.js
SAML 2.0 (SSO + SLO) samlify 2.13.1 /idp/saml Built; covered by test/samlGate.js
Multi-tenant (per-tenant issuer/keys/cert) n/a per-tenant schema Built; verified on Postgres
Admin UI (clients, groups, SAML SPs, LDAP service) n/a /admin/idp Built (CSRF-protected, role-gated)

Identity and groups: single source of truth

Saltcorn's user table is the only account store. The IdP never provisions new users; it authenticates existing Saltcorn users and projects their identity into whichever protocol the relying party speaks.

Group membership is computed once and reused across all three protocols. The "effective groups" for a user are:

  • the user's Saltcorn role, exposed with a role: prefix (for example role:admin), plus
  • membership in custom IdP groups (tables _idp_groups and _idp_group_members), exposed with a group: prefix (for example group:engineering).

That single list flows out as:

  • the OIDC groups claim (when the groups scope is granted),
  • the LDAP memberOf attribute (and groupOfNames entries), and
  • the SAML groups attribute.

Because roles and custom groups carry distinct prefixes, a role and a group of the same name never collide.


Quick start (development)

These steps mirror the local dev setup. The plugin source lives at /home/scott/claude/saltcorn/idp and is a sibling of the upstream Saltcorn checkout.

1. Install the plugin into the SQLite dev instances

The shared reinstallIdp.sh script installs saltcorn-idp into both the MAIN (.dev-state) and TEST (.dev-state-test) instances. Run it with the servers stopped (install is centralized here because the plugins folder is shared and saltcorn install-plugin needs an absolute -d path and clean node_modules symlinks):

cd /home/scott/claude/saltcorn
./reinstallIdp.sh

2. Run a dev instance

cd /home/scott/claude/saltcorn
./startServer.sh        # MAIN: SQLite, port 3000, LDAPS on 1636
./startServerTest.sh    # TEST: SQLite, port 3001, no LDAP
./startServerPg.sh      # PG:   Postgres multi-tenant, port 3002, LDAPS on 1637

On first load per tenant, onLoad (see index.js) creates the _idp_* tables, bootstraps the RSA signing key, ensures the SAML certificate, registers the /idp/ CSRF bypass, and starts the LDAP listener if SALTCORN_IDP_LDAP_PORT is set.

For the Postgres multi-tenant instance, register the plugin into each tenant schema (tenants must already exist and the plugin must be installed into the PG public schema once):

cd /home/scott/claude/saltcorn
source .dev-state-pg/env.sh
saltcorn install-plugin -d ./idp        # one-time, public schema
./idp/scripts/installIdpTenant.sh t1 t2 # per tenant (or '*' for all)

3. Run the gates

cd /home/scott/claude/saltcorn/idp

npm test          # OIDC + groups end-to-end (test/e2e.js)   -- needs :3000 and :3001
npm run test:mt   # multi-tenant OIDC (test/mtGate.js)       -- needs :3002 (Postgres)

node test/ldapGate.js              # LDAP (needs :3000 and LDAPS :1636)
node test/samlGate.js              # SAML 2.0 (needs :3000)
node test/ldapMultiTenantGate.js   # multi-tenant LDAP (needs :3002 and LDAPS :1637; self-skips if port unreachable)

npm test and npm run test:mt are the only scripts defined in package.json; the LDAP and SAML gates are invoked directly with node.


Configuration summary (key environment variables)

Variable Required Default Purpose
SALTCORN_SESSION_SECRET Yes n/a (falls back to Saltcorn session_secret config) Derives the at-rest KEK (AES-256-GCM) that seals private keys, SAML keys, and client/LDAP secrets, and the oidc-provider cookie keys. Rotating it invalidates all sealed data.
SALTCORN_IDP_LDAP_PORT No unset (LDAP disabled) Enables the LDAPS listener on this port. Each dev instance uses its own port to avoid collisions.
SALTCORN_IDP_LDAP_HOST No 127.0.0.1 Interface the LDAPS listener binds to. Loopback by default; set to 0.0.0.0 to expose LDAP on the network.

OIDC issuer derivation also honors Saltcorn's global base_url config. In any multi-tenant or proxied deployment, set base_url; the request-host fallback is vulnerable to Host-header injection. See lib/oidc/discovery.js.

The signing algorithm is RS256 over an RSA-2048 key per tenant; key lifecycle states are active, retiring, and retired (see lib/constants.js).


Path namespaces

Base path Scope CSRF
/idp Public OIDC/OAuth2/SAML + machine endpoints Exempt (registered in disable_csrf_routes)
/idp/saml SAML metadata / SSO / SLO / IdP-init Exempt
/admin/idp Admin UI (browser) Protected; gated to role_id 1

LDAP is not an HTTP path; it is a separate LDAPS listener enabled by SALTCORN_IDP_LDAP_PORT.


Documentation index

Deep-dive docs live under ./docs/. Each focuses on one subsystem; this README is the entry point.

Document Covers
./docs/architecture.md Integration model, module layout, the public /idp vs admin /admin/idp routing split, identity/groups single source of truth, and a multi-tenancy overview
./docs/oidc.md OIDC/OAuth2: discovery, JWKS, authorization-code + PKCE flow, the storage adapter, client registry, claims, and interaction (login/consent) handlers
./docs/ldap.md LDAP server: LDAPS startup, directory tree, bind (user + service account), search, DoS guards, and DN escaping
./docs/saml.md SAML 2.0: metadata, SP-initiated and IdP-initiated SSO, SLO, the SP registry + ACS allow-list, AuthnRequest signature verification, and XML hardening
./docs/configuration.md Configuration reference: environment variables, tunable constants, the _idp_* database tables, and the admin UI pages
./docs/operations.md Operations / dev environment: the three dev instances, starting and stopping, the plugin install workflow, multi-tenant host routing, and known issues
./docs/testing.md Test gates: scope, prerequisites, and what each gate asserts
./docs/security.md Security posture: dependency pinning/overrides, XXE defenses, hardening guards, and attack-vector summary

See also VENDORING.md for the dependency-ownership and security rationale behind the pinned ldapjs, samlify, xml-crypto, and @xmldom/xmldom versions.