169 lines
6.8 KiB
JavaScript
169 lines
6.8 KiB
JavaScript
// Constants for the saltcorn-idp plugin.
|
|
//
|
|
// One source of truth for plugin metadata, route base paths, table names, and
|
|
// signing parameters shared across the lib/ modules. Crypto byte-sizes live in
|
|
// crypto.js; protocol/policy values live here.
|
|
|
|
const PLUGIN_NAME = "saltcorn-idp";
|
|
const PLUGIN_VERSION = "0.0.1";
|
|
|
|
// Public OIDC/OAuth2 + machine endpoints live under this path and are
|
|
// CSRF-exempt. Admin (browser, CSRF-protected) pages live under ADMIN_BASE_PATH.
|
|
const IDP_BASE_PATH = "/idp";
|
|
const ADMIN_BASE_PATH = "/admin/idp";
|
|
|
|
// Defence-in-depth rate limit on admin mutation POSTs (on top of the role gate):
|
|
// a generous per-session sliding window -- normal admin use / the gates never
|
|
// approach it, but it caps automated abuse of a compromised admin session.
|
|
const ADMIN_RATE_MAX = 200;
|
|
const ADMIN_RATE_WINDOW_MS = 60 * 1000;
|
|
|
|
// Well-known discovery + JWKS endpoints (relative to the server root).
|
|
const WELL_KNOWN_OPENID = IDP_BASE_PATH + "/.well-known/openid-configuration";
|
|
// oidc-provider serves JWKS at the mount-relative /jwks (not under /.well-known);
|
|
// the discovery document advertises this as jwks_uri.
|
|
const JWKS_PATH = IDP_BASE_PATH + "/jwks";
|
|
|
|
// OIDC flow endpoints (served by oidc-provider via delegation) + the interaction
|
|
// (login/consent) endpoint we host ourselves.
|
|
const AUTH_PATH = IDP_BASE_PATH + "/auth";
|
|
const AUTH_RESUME_PATH = IDP_BASE_PATH + "/auth/:uid";
|
|
const TOKEN_PATH = IDP_BASE_PATH + "/token";
|
|
const USERINFO_PATH = IDP_BASE_PATH + "/me";
|
|
const INTERACTION_PATH = IDP_BASE_PATH + "/interaction/:uid";
|
|
const INTERACTION_CONFIRM_PATH = IDP_BASE_PATH + "/interaction/:uid/confirm";
|
|
|
|
// Plugin tables (all prefixed _idp_, created idempotently in onLoad).
|
|
const TABLE_ENV = "_idp_env";
|
|
const TABLE_KEYS = "_idp_keys";
|
|
const TABLE_OIDC_STORE = "_idp_oidc_store";
|
|
const TABLE_GROUPS = "_idp_groups";
|
|
const TABLE_GROUP_MEMBERS = "_idp_group_members";
|
|
const TABLE_CLIENTS = "_idp_clients";
|
|
|
|
// Signing.
|
|
const SIGNING_ALG = "RS256";
|
|
const RSA_MODULUS_BITS = 2048;
|
|
|
|
// Key lifecycle states.
|
|
const KEY_STATUS = {
|
|
ACTIVE: "active",
|
|
RETIRING: "retiring",
|
|
RETIRED: "retired"
|
|
};
|
|
|
|
// Signing-key rotation grace window. On rotation the prior ACTIVE key is marked
|
|
// RETIRING with retire_after = now + this window; it stays in JWKS (so tokens it
|
|
// signed still verify) until the window elapses, after which retireExpiredKeys()
|
|
// flips it RETIRED (dropped from JWKS). Sized to comfortably exceed the longest
|
|
// id_token lifetime so no live token is ever orphaned by rotation.
|
|
const KEY_RETIRE_GRACE_MS = 24 * 60 * 60 * 1000;
|
|
|
|
// LDAP (Phase 4). An instance enables LDAP by setting the port env var (so the
|
|
// dev instances don't collide on one port); the directory tree mirrors Saltcorn
|
|
// users (ou=people) and groups (ou=groups).
|
|
const LDAP_BASE_DN = "dc=saltcorn,dc=local";
|
|
const LDAP_PEOPLE_OU = "ou=people," + LDAP_BASE_DN;
|
|
const LDAP_GROUPS_OU = "ou=groups," + LDAP_BASE_DN;
|
|
const LDAP_PORT_ENV = "SALTCORN_IDP_LDAP_PORT";
|
|
// Interface the LDAPS listener binds to. Defaults to loopback so LDAP is NOT
|
|
// network-exposed unless an operator explicitly opts in (e.g. "0.0.0.0" to serve
|
|
// LDAP clients on other hosts).
|
|
const LDAP_HOST_ENV = "SALTCORN_IDP_LDAP_HOST";
|
|
const LDAP_DEFAULT_HOST = "127.0.0.1";
|
|
|
|
// LDAP DoS guards: cap total inbound bytes per connection (the BER parser has no
|
|
// size limit) and the search-filter nesting depth (the filter parser recurses
|
|
// without a depth bound). A multi-tenant deployment encodes the tenant as an
|
|
// extra dc component (dc=<tenant>,dc=saltcorn,dc=local); the bare base = default.
|
|
const LDAP_MAX_MSG_BYTES = 256 * 1024;
|
|
const LDAP_MAX_FILTER_DEPTH = 32;
|
|
const TABLE_LDAP_SERVICE = "_idp_ldap_service";
|
|
|
|
// LDAP bind-retry: only the cluster primary binds the listener, so a transient
|
|
// EADDRINUSE (e.g. the prior process's socket lingering across a fast restart)
|
|
// would otherwise leave LDAP silently down with no fallback. Retry a bounded
|
|
// number of times with linear backoff, then warn LOUDLY. Delay = base * attempt.
|
|
const LDAP_BIND_MAX_ATTEMPTS = 5;
|
|
const LDAP_BIND_RETRY_BASE_MS = 500;
|
|
// Per-connection idle timeout: a connection that sends no data for this long is
|
|
// destroyed, so an attacker cannot hoard the connection pool with idle/slow-loris
|
|
// sockets (works with LDAP_MAX_MSG_BYTES, which bounds a never-completing message).
|
|
const LDAP_IDLE_TIMEOUT_MS = 30 * 1000;
|
|
// Cap the number of directory entries a single search loads/returns, bounding
|
|
// memory; past this the search returns sizeLimitExceeded. (MVP loads users into
|
|
// memory and filters there; a production build would push the filter to SQL.)
|
|
const LDAP_MAX_SEARCH_RESULTS = 2000;
|
|
|
|
// SAML (Phase 5). The IdP entityID is <issuer>/saml; SSO + metadata under /idp/saml.
|
|
const TABLE_SAML = "_idp_saml";
|
|
const TABLE_SAML_SPS = "_idp_saml_sps";
|
|
const SAML_METADATA_PATH = IDP_BASE_PATH + "/saml/metadata";
|
|
const SAML_SSO_PATH = IDP_BASE_PATH + "/saml/sso";
|
|
const SAML_SLO_PATH = IDP_BASE_PATH + "/saml/slo";
|
|
const SAML_INIT_PATH = IDP_BASE_PATH + "/saml/init";
|
|
|
|
// SAML DoS guards: cap the base64 SAMLRequest/SAMLResponse we accept (before
|
|
// decoding) and the inflated XML size (deflate can expand ~1000:1, so an
|
|
// unbounded inflateRawSync on an unauthenticated endpoint is a memory bomb).
|
|
const SAML_MAX_MSG_B64_BYTES = 64 * 1024;
|
|
const SAML_MAX_XML_BYTES = 256 * 1024;
|
|
|
|
// AuthnContext class for password login + the signature algorithm we require on
|
|
// signed SAML messages (RSA-SHA256). One source of truth for these URNs.
|
|
const SAML_AUTHN_CONTEXT = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
|
|
const SAML_SIG_ALG = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
|
|
|
|
// Minimum acceptable xml-crypto version (2025 SAML-signature CVEs patched).
|
|
const XML_CRYPTO_MIN = "6.1.2";
|
|
|
|
module.exports = {
|
|
PLUGIN_NAME,
|
|
PLUGIN_VERSION,
|
|
IDP_BASE_PATH,
|
|
ADMIN_BASE_PATH,
|
|
ADMIN_RATE_MAX,
|
|
ADMIN_RATE_WINDOW_MS,
|
|
WELL_KNOWN_OPENID,
|
|
JWKS_PATH,
|
|
AUTH_PATH,
|
|
AUTH_RESUME_PATH,
|
|
TOKEN_PATH,
|
|
USERINFO_PATH,
|
|
INTERACTION_PATH,
|
|
INTERACTION_CONFIRM_PATH,
|
|
TABLE_ENV,
|
|
TABLE_KEYS,
|
|
TABLE_OIDC_STORE,
|
|
TABLE_GROUPS,
|
|
TABLE_GROUP_MEMBERS,
|
|
TABLE_CLIENTS,
|
|
SIGNING_ALG,
|
|
RSA_MODULUS_BITS,
|
|
KEY_STATUS,
|
|
KEY_RETIRE_GRACE_MS,
|
|
LDAP_BASE_DN,
|
|
LDAP_PEOPLE_OU,
|
|
LDAP_GROUPS_OU,
|
|
LDAP_PORT_ENV,
|
|
LDAP_HOST_ENV,
|
|
LDAP_DEFAULT_HOST,
|
|
LDAP_MAX_MSG_BYTES,
|
|
LDAP_MAX_FILTER_DEPTH,
|
|
TABLE_LDAP_SERVICE,
|
|
LDAP_BIND_MAX_ATTEMPTS,
|
|
LDAP_BIND_RETRY_BASE_MS,
|
|
LDAP_IDLE_TIMEOUT_MS,
|
|
LDAP_MAX_SEARCH_RESULTS,
|
|
TABLE_SAML,
|
|
TABLE_SAML_SPS,
|
|
SAML_METADATA_PATH,
|
|
SAML_SSO_PATH,
|
|
SAML_SLO_PATH,
|
|
SAML_INIT_PATH,
|
|
SAML_MAX_MSG_B64_BYTES,
|
|
SAML_MAX_XML_BYTES,
|
|
SAML_AUTHN_CONTEXT,
|
|
SAML_SIG_ALG,
|
|
XML_CRYPTO_MIN
|
|
};
|