| docs | ||
| lib | ||
| scripts | ||
| test | ||
| .gitattributes | ||
| .gitignore | ||
| index.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| VENDORING.md | ||
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 examplerole:admin), plus - membership in custom IdP groups (tables
_idp_groupsand_idp_group_members), exposed with agroup:prefix (for examplegroup:engineering).
That single list flows out as:
- the OIDC
groupsclaim (when thegroupsscope is granted), - the LDAP
memberOfattribute (andgroupOfNamesentries), and - the SAML
groupsattribute.
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.