wmantly f45349e0cd Fix uid/gidNumber allocation crash, add a configurable id floor (#44)
Reported: creating any user via the API failed with

  {"name":"InvalidSyntaxError","message":"gidNumber: value #0 invalid per syntax Code: 0x15"}

Root cause: addPosixGroup() computes the next gidNumber as
`Math.max(...groups.map(i => i.gidNumber)) + 1`. theta-env's
bootstrap.js creates the first admin via raw ldapadd with a hardcoded
uidNumber/gidNumber (10000) directly on the user entry, but never
creates a matching posixGroup entry -- so on a theta-env-bootstrapped
directory there are zero posixGroup entries, `Math.max()` on an empty
array is `-Infinity` in JS (not 0), and `-Infinity + 1` stringifies to
"-Infinity" -- an invalid LDAP integer, rejected by the directory. This
broke every single user creation, not just this one.

Separately: the reporter's intended scheme is for organically-created
users to start at uidNumber/gidNumber 1500, distinct from the
bootstrap admin's reserved 10000. Fixing the crash with a bare "floor
of 1500" alone wouldn't achieve that, since addPosixAccount's own
Math.max() would still find the admin's posixAccount entry (uidNumber
10000, found via a different, correctly-indexed search) and allocate
10001 for the next user.

Added a shared nextPosixId(entries, key) helper: takes the highest
existing value strictly below conf.ldap.uidGidReservedFloor (default
9000) plus one, or conf.ldap.uidGidMin (default 1500) if there are no
such entries. Ids at/above the reserved floor -- like the bootstrap
admin's 10000 -- are ignored entirely when computing the next
available number, so real users always start at 1500 and grow upward
regardless of the admin's reserved id.

Verified against a real theta-env deployment end to end:
- Reproduced the exact reported crash on a fresh bootstrap
- After the fix: first real user gets uidNumber/gidNumber "1500",
  second gets "1501" -- admin's 10000 never enters the calculation
- New unit tests (nodejs/tests/posix_id.test.js, no LDAP required):
  6/6 pass, covering the empty-array case, the reserved-floor
  exclusion, and the NaN-from-missing-value case
- npm test: 18/18 passing tests still pass (unchanged); the other 155
  failures are pre-existing/environmental (no LDAP server in this
  sandbox) -- confirmed via git stash before starting this fix
2026-07-14 23:03:59 -04:00
2026-07-11 00:54:49 -04:00

SSO Manager

A self-hosted OpenID Connect provider with a bundled OpenLDAP directory and a web management UI — for home labs and small businesses that want their own identity provider instead of a hosted one.

It gives you one place to manage your users and groups, one login (OIDC) that your modern apps can use, and one LDAP directory your older or odder apps can bind to directly. Everything runs on your own hardware; there is no phone-home, no hosted control plane, and no per-user pricing.

Setting up the whole stack (this SSO + the theta42/proxy in front of it) with one command? Skip to theta-env — its setup.sh wires the two together and generates the config for you.

Features

  • OpenID Connect / OAuth 2.0 provider — issue your own access, refresh, and ID tokens; protect your apps with standard OIDC login. Discovery document at /.well-known/openid-configuration.
  • Bundled OpenLDAP directory — users, groups, POSIX accounts (posixAccount/inetOrgPerson), SSH public keys, and sudo roles, with memberOf + referential-integrity overlays. This is your single source of truth for identity, not a sidecar.
  • Web management UI — manage users, groups, and OAuth clients from a browser; invite and password-reset flows over email; user self-service for profile and API tokens.
  • LDAPS for legacy apps — apps that bind LDAP directly (Gitea, Emby, and anything else that speaks LDAP) use LDAPS (636) or StartTLS against the same directory, so you don't maintain a second user database for them.
  • Personal access tokens — any user can mint a long-lived bearer token to drive the management API from scripts or CI, scoped to their own permissions.
  • All-in-one Docker image — app + OpenLDAP + Redis in one container, or run the pieces separately against your own LDAP/Redis via app_* env config.

Why this over the alternatives

Tools like Keycloak, Authentik, Authelia, or Zitadel are OIDC providers, but LDAP is either a paid feature, a federation target you have to run separately, or absent. If your stack already has apps that speak LDAP directly (or you just want one real directory as the source of truth), you end up running two identity systems and keeping them in sync.

SSO Manager bundles the OpenLDAP directory with the OIDC provider, so OIDC apps and LDAP apps read from the same users and groups. The trade-off is scope: it is intentionally small and self-hosted, not an enterprise IAM suite — no fancy workflow engine, no hosted SaaS. If you want a lightweight, self-contained identity provider with a real LDAP backend, that is the niche.

Quick start

Three ways to run it, in order of how much it sets up for you:

theta-env composes this SSO Manager with the theta42/proxy (an OIDC-protected reverse proxy) and generates all the config from a single setup.env — you enter your domain once and it fills in the LDAP DNs, hostnames, OAuth issuer, and random secrets consistently:

git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env
cp setup.env.example setup.env   # set CFG_DOMAIN to your domain
./setup.sh                       # generates ./config/, builds + bootstraps + starts both

See the theta-env README for the full first-run flow, DNS/port requirements, and backups.

2. Standalone, in Docker

The all-in-one image bundles the app, OpenLDAP, and Redis. Copy the example secrets file, fill in your values, and build:

git clone https://github.com/theta42/sso-manager-node.git
cd sso-manager-node
mkdir -p config && chmod 700 config
cp secrets.js.example config/sso-secrets.js
$EDITOR config/sso-secrets.js     # set ldap.bindPassword, oauth.jwtSecret, ...
docker compose up -d --build

The web UI comes up at http://localhost:3001. To kick the tires with no config file at all, the entrypoint falls back to safe defaults (dc=example,dc=com, admin password admin, an auto-generated JWT) — fine for a local test, not for production.

Your domain is entered once, as the LDAP base DN (stack.ldapBaseDn); the other LDAP DNs and the OAuth issuer derive from it and must stay consistent. See DEPLOYMENT.md for the full config reference, the app_* env vars, LDAPS/TLS, and backups.

3. Bare metal on Debian/Ubuntu

install.sh is an idempotent installer: it installs Node.js 20.x and OpenLDAP, configures the directory (modules, overlays, schema, the SSO groups), deploys the app to /opt/sso-manager, and creates a systemd unit.

The only thing it requires is the LDAP admin password; the domain (base DN) defaults to dc=example,dc=com if you don't pass one:

sudo ./install.sh -p 'your-ldap-password' -b 'dc=yourdomain,dc=com'
sudo systemctl enable --now sso-manager
curl http://localhost:3001/health    # -> {"status":"ok"}

Run sudo ./install.sh -h for all flags (-n org name, -o port, -j JWT secret, -s SMTP, --skip-ldap to use an existing LDAP, --dry-run). Re-run it to update. Full details in DEPLOYMENT.md under Method 2: Bare metal.

Architecture

┌─────────────┐
│  Browser /  │
│  OIDC apps  │
└──────┬──────┘
       │ HTTP/HTTPS
       ▼
┌────────────────────────┐      ┌─────────────┐
│  Express SSO Manager   │◄────►│   Redis     │
│  - OIDC provider       │      │ - sessions  │
│  - web UI (:3001)      │      │ - models    │
│  - management API      │      └─────────────┘
└────────┬───────────────┘
         │ ldapi/ldap (localhost)
         ▼
┌────────────────────────┐
│  OpenLDAP (slapd)      │
│  - users / groups      │
│  - LDAPS :636          │─── legacy apps bind directly
│  - StartTLS :389       │
└────────────────────────┘

Documentation

The nitty LDAP details (overlay setup, the custom theta42Person schema, the required groups, LDAPS/TLS, direct-bind service accounts) live in:

If you are pointing the app at your own existing LDAP server, see LDAP requirements in DEPLOYMENT.md — the directory needs the pw-sha2, ppolicy, memberof, and refint modules plus a small custom schema. The bundled Docker image and install.sh set all of that up for you. Required groups: app_sso_admin (full admin), app_sso_oauth_admin (manage OAuth clients only), app_sso_invite (invitation management) — see DEPLOYMENT.md for the full setup.

Development

cd nodejs
npm install
npm run dev      # nodemon auto-reload
npm test         # jest test suite

License

MIT — see LICENSE.

S
Description
LDAP GUI and API manager for use with SSO.
Readme MIT 58 MiB
Languages
JavaScript 63.6%
EJS 31%
Shell 5.2%
CSS 0.2%