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

16 KiB

Configuration Reference

This is the complete configuration reference for the saltcorn-idp plugin. It covers the environment variables the plugin reads, the tunable constants baked into lib/constants.js and lib/crypto.js, every _idp_* database table and its columns, and the admin UI pages. Every value below is grounded in the plugin source; file and constant names are given so they can be verified.

For protocol-specific detail see the sibling docs: ./architecture.md (integration model and multi-tenancy overview), ./oidc.md, ./ldap.md, ./saml.md, ./operations.md, and ./security.md.


Environment variables

The plugin itself reads only the three variables below. They are read in lib/crypto.js (SALTCORN_SESSION_SECRET) and lib/ldap/server.js / lib/constants.js (the two LDAP variables).

Variable Required Default Effect
SALTCORN_SESSION_SECRET Yes (see note) None. Falls back to Saltcorn's session_secret config when unset; if neither is available, deriving the KEK throws "saltcorn-idp: SALTCORN_SESSION_SECRET not available; cannot derive KEK". Source material for two derivations: (1) the at-rest KEK (AES-256-GCM, via HKDF-SHA256) that seals RSA signing keys, the SAML private key, OIDC client secrets, and the LDAP service-account password; (2) the deterministic oidc-provider cookie-signing keys (so interaction/session cookies survive a restart). Rotating this value re-derives the KEK and invalidates all previously sealed data.
SALTCORN_IDP_LDAP_PORT No Unset. When unset, startLdap() returns early and no LDAP listener is started. Enables the in-process LDAPS listener and sets its TCP port. Parsed with parseInt(..., 10); a non-finite value is ignored (no listener). The listener binds only in the cluster primary process. Constant name: LDAP_PORT_ENV.
SALTCORN_IDP_LDAP_HOST No 127.0.0.1 (constant LDAP_DEFAULT_HOST). An empty or whitespace-only value also falls back to the default. Interface the LDAPS listener binds to. Loopback by default so LDAP is not network-exposed; set to 0.0.0.0 to serve LDAP clients on other hosts. Binding beyond loopback logs a network-exposure warning. Constant name: LDAP_HOST_ENV.

Notes:

  • SALTCORN_SESSION_SECRET "required" means the plugin cannot seal/unseal data without it. In a normal Saltcorn deployment the session_secret config provides the fallback, so the explicit environment variable is required only where that config is not present (for example, code paths outside a request context).
  • SALTCORN_JWT_SECRET is not read by this plugin. It appears in the Postgres dev instance's env.sh but is consumed by Saltcorn core, not by saltcorn-idp. (Verified: no reference to SALTCORN_JWT_SECRET exists in the plugin source.)
  • SALTCORN_MULTI_TENANT is likewise not read directly by the plugin. The plugin detects multi-tenancy by calling Saltcorn's db.is_it_multi_tenant() (used in lib/ldap/tenant.js), which reflects Saltcorn's own configuration.

These are Saltcorn config values (read/written through getState().getConfig / setConfig), not environment variables, but they affect plugin behavior:

Config key Effect
base_url Preferred source for OIDC issuer and SAML entityID derivation (lib/oidc/discovery.js). When unset, the issuer is derived from the request Host header and a warning is logged: "base_url not set; deriving issuer from request Host ... Set base_url to prevent Host-header issuer poisoning." Set base_url in any multi-tenant or proxied deployment.
session_secret Fallback source for the KEK when SALTCORN_SESSION_SECRET is unset (lib/crypto.js).
disable_csrf_routes The plugin appends /idp/ to this list during onLoad so the machine-driven /idp endpoints are CSRF-exempt (index.js). Admin pages under /admin/idp remain CSRF-protected.

Tunable constants

All values below are defined in lib/constants.js unless marked otherwise. Crypto byte sizes live in lib/crypto.js.

Signing

Constant Value Effect
SIGNING_ALG RS256 Algorithm for OIDC id_token signing (and the required SAML signature is RSA-SHA256, below).
RSA_MODULUS_BITS 2048 RSA key size for generated signing keypairs.
KEY_STATUS.ACTIVE "active" Key currently used to sign new tokens.
KEY_STATUS.RETIRING "retiring" Key no longer signing but still published in JWKS for verification.
KEY_STATUS.RETIRED "retired" Key fully retired (not in JWKS).

Crypto byte sizes (lib/crypto.js)

Constant Value Effect
GCM_ALGORITHM aes-256-gcm At-rest sealing cipher.
HKDF_HASH sha256 Hash for HKDF key derivation and the session-secret cache hash.
IV_BYTES 12 AES-GCM IV length (96-bit).
KEK_BYTES 32 Derived KEK length (256-bit).
KID_BYTES 16 Random bytes for a generated signing key id (kid), hex-encoded.
KEK_INFO saltcorn-idp:at-rest:aes-gcm-key:v1 HKDF info for KEK derivation (domain separation).
KEK_SALT saltcorn-idp:at-rest:aes-gcm-salt:v1 HKDF salt for KEK derivation.

LDAP

Constant Value Effect
LDAP_BASE_DN dc=saltcorn,dc=local Directory base DN. Multi-tenant DNs add a dc=<tenant> component before the base.
LDAP_PEOPLE_OU ou=people,dc=saltcorn,dc=local OU for user entries.
LDAP_GROUPS_OU ou=groups,dc=saltcorn,dc=local OU for group entries.
LDAP_MAX_MSG_BYTES 262144 (256 * 1024) DoS guard: cap on inbound bytes per LDAP message (the BER parser has no size limit). The byte counter is reset on each parsed-message boundary, so this is a per-message cap, not a connection-lifetime quota -- a per-connection counter would let an attacker kill a legitimate long-lived connection after a few normal operations. The connection is destroyed if a single message exceeds the cap. Enforced in lib/ldap/vendor.js; see ./security.md for the full rationale.
LDAP_MAX_FILTER_DEPTH 32 DoS guard: maximum search-filter nesting depth.
LDAP_BIND_MAX_ATTEMPTS 5 Max listener bind attempts on a transient failure (EADDRINUSE/EACCES).
LDAP_BIND_RETRY_BASE_MS 500 Bind retry backoff base; delay = base * attempt (linear).

The LDAP simple-bind handler also enforces a credential length cap, defined locally in lib/ldap/bind.js rather than in lib/constants.js:

Constant Value Effect
MAX_CRED_LEN (lib/ldap/bind.js) 1024 DoS/abuse guard on the bind handler: a bind with an empty DN, an empty password, or a password longer than this many characters is rejected as invalid credentials (and counts as a failure for the per-IP lockout) before any bcrypt or constant-time comparison runs.

Additional LDAP runtime values set in lib/ldap/server.js:

Setting Value Effect
server.maxConnections 256 Maximum concurrent LDAP connections.
self-signed cert RSA 2048-bit, SHA256 Generated per startup for LDAPS (dev); production supplies a cert via config.

SAML

Constant Value Effect
SAML_MAX_MSG_B64_BYTES 65536 (64 * 1024) DoS guard: cap on the base64 SAMLRequest/SAMLResponse accepted before decoding.
SAML_MAX_XML_BYTES 262144 (256 * 1024) DoS guard: cap on inflated XML size (DEFLATE can expand roughly 1000:1; prevents a memory bomb).
SAML_AUTHN_CONTEXT urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport AuthnContext class advertised for password login.
SAML_SIG_ALG http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 Required signature algorithm (RSA-SHA256) on signed SAML messages.
XML_CRYPTO_MIN 6.1.2 Minimum acceptable xml-crypto version (2025 SAML-signature CVEs patched); asserted at load.

Plugin metadata and path namespaces

Constant Value
PLUGIN_NAME saltcorn-idp
PLUGIN_VERSION 0.0.1
IDP_BASE_PATH /idp
ADMIN_BASE_PATH /admin/idp

The admin role gate is ADMIN_ROLE_ID = 1, defined in lib/adminUi.js (admin pages require req.user.role_id === 1).


Database tables

All tables are prefixed _idp_ and created idempotently during onLoad by createAllTables() in lib/schema.js. Raw DDL is schema-qualified with db.getTenantSchemaPrefix() so each Postgres tenant gets its own tables; on SQLite the prefix is empty. Sealed key/secret material is stored as hex TEXT. The auto-increment primary key on _idp_groups uses integer on SQLite and serial on Postgres.

_idp_env

Singleton per-tenant row tracking first-run bootstrap state and an instance label.

Column Type Notes
env_id TEXT PRIMARY KEY UUID assigned on first init (crypto.randomUUID()).
env_label TEXT Optional instance label; initialized to NULL.
created_at TEXT NOT NULL ISO 8601 timestamp.
bootstrapped_at TEXT ISO 8601 timestamp set by markBootstrapped; NULL until first bootstrap.

_idp_keys

Per-tenant RSA signing keypairs and lifecycle. Index: _idp_keys_status on status.

Column Type Notes
kid TEXT PRIMARY KEY Key id (hex).
alg TEXT NOT NULL Signing algorithm (RS256).
public_jwk TEXT NOT NULL Public JWK, JSON-stringified.
private_ciphertext TEXT NOT NULL Sealed private key PEM (hex).
private_iv TEXT NOT NULL AES-GCM IV (hex).
private_tag TEXT NOT NULL AES-GCM auth tag (hex).
status TEXT NOT NULL DEFAULT 'active' One of active, retiring, retired.
created_at TEXT NOT NULL ISO 8601 timestamp.
retire_after TEXT Optional retire-after timestamp.

_idp_oidc_store

Single table backing the oidc-provider storage adapter (all model types). Indexes: _idp_oidc_store_uid on uid, _idp_oidc_store_grant on grant_id, _idp_oidc_store_usercode on user_code.

Column Type Notes
model TEXT NOT NULL oidc-provider model type. Part of PK.
id TEXT NOT NULL Instance id. Part of PK.
payload TEXT NOT NULL Serialized model state (JSON).
uid TEXT Interaction uid (nullable).
grant_id TEXT Grant reference (nullable).
user_code TEXT Device user code (nullable).
expires_at INTEGER Unix epoch seconds (nullable).
PRIMARY KEY (model, id) Composite.

_idp_groups

Custom groups (in addition to Saltcorn roles).

Column Type Notes
id integer/serial PRIMARY KEY Auto-increment (SQLite integer, Postgres serial).
name TEXT NOT NULL UNIQUE Group name.
description TEXT Optional description.
created_at TEXT NOT NULL ISO 8601 timestamp.

_idp_group_members

User-to-group junction. Index: _idp_group_members_user on user_id.

Column Type Notes
group_id INTEGER NOT NULL Part of PK.
user_id INTEGER NOT NULL Saltcorn user id. Part of PK.
PRIMARY KEY (group_id, user_id) Composite.

_idp_clients

Registered OIDC relying parties. Confidential clients' secrets are sealed at rest (hex columns); public clients (token_auth_method = 'none') have none.

Column Type Notes
client_id TEXT PRIMARY KEY Client identifier.
label TEXT Display name.
token_auth_method TEXT NOT NULL DEFAULT 'none' none, client_secret_basic, or client_secret_post.
redirect_uris TEXT NOT NULL JSON array.
grant_types TEXT NOT NULL JSON array.
response_types TEXT NOT NULL JSON array.
scope TEXT Space-separated scope string.
secret_ciphertext TEXT Sealed secret (hex); NULL for public clients.
secret_iv TEXT AES-GCM IV (hex); NULL for public clients.
secret_tag TEXT AES-GCM auth tag (hex); NULL for public clients.
created_at TEXT NOT NULL ISO 8601 timestamp.

_idp_saml

Per-tenant SAML signing material (singleton row): a self-signed X.509 cert advertised in IdP metadata plus its sealed private key used to sign assertions.

Column Type Notes
id TEXT PRIMARY KEY Fixed row id.
cert TEXT NOT NULL X.509 certificate (PEM, plaintext).
private_ciphertext TEXT NOT NULL Sealed private key (hex).
private_iv TEXT NOT NULL AES-GCM IV (hex).
private_tag TEXT NOT NULL AES-GCM auth tag (hex).
created_at TEXT NOT NULL ISO 8601 timestamp.

_idp_saml_sps

Registered SAML service providers. The IdP only issues an assertion to a registered SP and only to one of its allow-listed ACS URLs. The optional signing_cert (public, not sealed) enables AuthnRequest signature verification; want_authn_requests_signed gates enforcement.

Column Type Notes
entity_id TEXT PRIMARY KEY SP entityID.
label TEXT Display name.
acs_urls TEXT NOT NULL JSON array of allow-listed ACS URLs.
signing_cert TEXT Public X.509 cert (PEM); nullable.
want_authn_requests_signed INTEGER NOT NULL DEFAULT 0 0/1 boolean; gates signature enforcement.
created_at TEXT NOT NULL ISO 8601 timestamp.

_idp_ldap_service

LDAP service-account credentials for the search-then-bind flow (single row). The password is sealed at rest (hex columns), like client secrets.

Column Type Notes
dn TEXT Service bind DN.
secret_ciphertext TEXT Sealed password (hex).
secret_iv TEXT AES-GCM IV (hex).
secret_tag TEXT AES-GCM auth tag (hex).
created_at TEXT NOT NULL ISO 8601 timestamp.

Admin UI pages

The admin UI is defined in lib/adminUi.js. All pages live under /admin/idp (constant ADMIN_BASE_PATH), are gated to role_id = 1, and use CSRF-protected browser forms (a hidden _csrf field is rendered via req.csrfToken()). Non-admins receive HTTP 403 admin only.

Path Method Page / action
/admin/idp GET Dashboard: env id, bootstrapped-at, issuer, active signing kid, signing alg, published JWKS key count, discovery and JWKS links.
/admin/idp/ GET Dashboard (alias).
/admin/idp/clients GET List OIDC clients and the registration form.
/admin/idp/clients/create POST Register a new OIDC client (returns the one-time secret for confidential clients).
/admin/idp/clients/delete POST Delete an OIDC client by client_id.
/admin/idp/groups GET List custom groups with members and add/remove member controls.
/admin/idp/groups/create POST Create a custom group by name.
/admin/idp/groups/delete POST Delete a group by id.
/admin/idp/groups/addmember POST Add a user to a group by email.
/admin/idp/groups/removemember POST Remove a user from a group.
/admin/idp/saml-sps GET List SAML service providers and the registration form.
/admin/idp/saml-sps/create POST Register a SAML SP (rejects empty entityID or empty ACS list).
/admin/idp/saml-sps/delete POST Delete a SAML SP by entity_id.
/admin/idp/ldap GET Show the configured LDAP service DN and set/clear forms (password never displayed).
/admin/idp/ldap/service POST Set the LDAP service-account DN and password (password sealed at rest).
/admin/idp/ldap/service/clear POST Clear the LDAP service account.

The shared navigation bar links: Dashboard, Clients, Groups, SAML SPs, LDAP.