178 lines
7.9 KiB
Markdown
178 lines
7.9 KiB
Markdown
# 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):
|
|
|
|
```bash
|
|
cd /home/scott/claude/saltcorn
|
|
./reinstallIdp.sh
|
|
```
|
|
|
|
### 2. Run a dev instance
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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/`](./docs/). Each focuses on one subsystem;
|
|
this README is the entry point.
|
|
|
|
| Document | Covers |
|
|
|----------|--------|
|
|
| [`./docs/architecture.md`](./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`](./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`](./docs/ldap.md) | LDAP server: LDAPS startup, directory tree, bind (user + service account), search, DoS guards, and DN escaping |
|
|
| [`./docs/saml.md`](./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`](./docs/configuration.md) | Configuration reference: environment variables, tunable constants, the `_idp_*` database tables, and the admin UI pages |
|
|
| [`./docs/operations.md`](./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`](./docs/testing.md) | Test gates: scope, prerequisites, and what each gate asserts |
|
|
| [`./docs/security.md`](./docs/security.md) | Security posture: dependency pinning/overrides, XXE defenses, hardening guards, and attack-vector summary |
|
|
|
|
See also [`VENDORING.md`](./VENDORING.md) for the dependency-ownership and
|
|
security rationale behind the pinned `ldapjs`, `samlify`, `xml-crypto`, and
|
|
`@xmldom/xmldom` versions.
|