Dockerize SSO Manager (all-in-one image) + GitHub Pages docs

All-in-one Dockerfile.openldap bundling the app + OpenLDAP + Redis in one
container, plus an idempotent bare-metal install.sh, and a Jekyll docs site
for GitHub Pages:
- Dockerfile.openldap (node:20-alpine; openldap + pw-sha2/ppolicy/memberof/
  refint; dumb-init PID 1; npm ci --omit=dev; tos.md copied to /).
- docker-entrypoint.sh: generate slapd.conf (mdb + overlays + TLS + indexes +
  access), self-signed LDAPS cert, seed directory tree + required groups,
  bundled redis, export app_* config, exec node.
- docker-compose.yml, .dockerignore, DEPLOYMENT.md, secrets.js.example.
- install.sh: idempotent Debian/Ubuntu bare-metal installer (Node 20.x,
  OpenLDAP, Redis, systemd unit) with flags + --dry-run/--skip-ldap/--skip-app.
- ops/ldif/: memberof/refint/tls/index/nodes/logging LDIFs.
- nodejs/conf/base.js: generic defaults (dc=example,dc=com / localhost /
  SSO Manager) so per-deployment values move to secrets.js or app_* env.
- nodejs/package.json: bump @simpleworkjs/conf to ^1.1.0 (app_* env overrides).
- nodejs/routes/index.js: /health endpoint for healthchecks.
- docs/: _config.yml + index/deployment/configuration/oauth/ldap pages
  (jekyll-theme-cayman) for GitHub Pages from /docs.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:03:16 -04:00
parent e28fb63784
commit fe9b7c168b
27 changed files with 2397 additions and 16 deletions
+45
View File
@@ -0,0 +1,45 @@
# Git
.git
.gitignore
# Development
.claude
*.md
# README.md and tos.md are both read at runtime (tos.md is loaded by
# routes/index.js at boot), so they must stay in the build context.
!README.md
!tos.md
# Tests
nodejs/tests/
nodejs/*.test.js
# Host dependency tree — let the image run a clean `npm ci`. Also avoids
# copying platform-wrong native modules (e.g. bcrypt built for the host OS).
nodejs/node_modules
# IDE
.vscode
.idea
*.swp
*.swo
# Logs
*.log
logs/
# OS
.DS_Store
Thumbs.db
# Docker (prevent recursive copy)
Dockerfile*
docker-compose.yml
.dockerignore
# Ops scripts (not needed in container)
ops/
# Secrets (mount at runtime instead)
nodejs/conf/secrets.js
secrets.js
+6
View File
@@ -85,3 +85,9 @@ ops/cookbooks/vendor
secrets.json
secrets.js
# Jekyll build artifact (GitHub Pages builds remotely; ignore locally)
docs/_site
# Jekyll build artifact (GitHub Pages builds remotely; ignore locally)
docs/_site
+339
View File
@@ -0,0 +1,339 @@
# Deployment Guide — SSO Manager
Two supported deployment methods:
1. **Docker** — a single all-in-one image bundling the app + OpenLDAP (`docker compose up`).
2. **Bare metal**`install.sh` on Debian/Ubuntu (installs Node.js, OpenLDAP, the app, and a systemd unit).
## How configuration works
The app loads configuration via [`@simpleworkjs/conf`](https://www.npmjs.com/package/@simpleworkjs/conf), which deep-merges, in order:
1. `conf/base.js` (committed, generic defaults)
2. `conf/<NODE_ENV>.js` (optional)
3. `conf/secrets.js` (gitignored — secrets + per-deployment values)
4. **`app_*` environment variables** — the highest-precedence layer
Any env var whose name starts with `app_` overrides the merged config. The rest
of the name is split on **double-underscore** (`__`) into a nested path. Values
are `JSON.parse`-coerced when possible (numbers, booleans, null, JSON) and kept
as raw strings otherwise. Examples:
| Env var | Sets | Type |
|---------|------|------|
| `app_ldap__url=ldap://host:389` | `conf.ldap.url` | string |
| `app_ldap__bindPassword=secret` | `conf.ldap.bindPassword` | string |
| `app_oauth__jwtSecret=...` | `conf.oauth.jwtSecret` | string |
| `app_smtp__secure=false` | `conf.smtp.secure` | boolean |
| `app_oauth__token_lifetime__access_token=3600` | `conf.oauth.token_lifetime.access_token` | number |
| `app_name=My SSO` | `conf.name` | string |
> **Requires `@simpleworkjs/conf` >= 1.1.0.** The Docker image will not honor
> `app_*` env vars on 1.0.0. Before building the image, refresh the app's
> dependency lock from the `nodejs/` directory:
> ```bash
> cd nodejs && npm install @simpleworkjs/conf@^1.1.0
> ```
---
## Method 1: Docker (all-in-one)
The image (`Dockerfile.openldap`) bundles OpenLDAP and the app in one container.
The app connects to the bundled slapd over `localhost:389` automatically; you only
need to set a few secrets.
### Setup
```bash
# Minimal: set an LDAP admin password and a JWT secret, then build + start.
LDAP_ADMIN_PASS='choose-a-strong-password' \
JWT_SECRET="$(openssl rand -hex 32)" \
docker compose up -d --build
```
For a customized deployment, put the overrides in a `.env` file next to
`docker-compose.yml`:
```env
LDAP_BASE_DN=dc=yourdomain,dc=com
LDAP_DOMAIN=yourdomain.com
LDAP_ADMIN_PASS=your-admin-password
ORG_NAME=Your Org
JWT_SECRET=your-jwt-secret
OAUTH_ISSUER=https://sso.yourdomain.com # browser-facing URL the proxy serves
LDAP_CERT_CN=sso.yourdomain.com # hostname LDAPS clients verify against
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USER=noreply@yourdomain.com
SMTP_PASS=your-smtp-password
SMTP_FROM=Your Org <noreply@yourdomain.com>
PORT=3001
LDAPS_PORT=636
# LDAP_PORT=389 # uncomment the 389 host mapping in compose if you need plain LAN binds
```
Then `docker compose up -d --build`.
### What the entrypoint does
`docker-entrypoint.sh` (run as the container entrypoint):
1. Generates a self-signed TLS cert (unless one is already present at
`LDAP_CERT_DIR`), generates a `slapd.conf` for the bundled OpenLDAP (`mdb`
database, `pw-sha2`/`ppolicy`/`memberof`/`refint` modules + overlays, TLS,
indexes, access controls), and starts `slapd -f /etc/openldap/slapd.conf`
listening on `ldap:///` (389) and `ldaps:///` (636).
2. Seeds the directory (base DN, `ou=people`/`ou=groups`/`ou=policies`, a default
`pwdPolicy`, and the required SSO groups `app_sso_admin`, `app_sso_invite`,
`app_sso_oauth_admin`) — idempotently, so container restarts are safe.
3. Starts a bundled Redis (the app uses `model-redis` for models/sessions), unless
`app_redis__host` is set (then it's expected to be external).
4. Exports `app_*` env vars so the app binds to the local slapd (any `app_*` you
set in the compose environment wins over the entrypoint's defaults).
5. `exec`s `node bin/www`.
### Access
- SSO Manager UI: `http://localhost:3001` (HTTP inside the container — put a TLS-terminating proxy in front for browser access)
- Health check: `http://localhost:3001/health``{"status":"ok"}`
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
- LDAP (internal, app↔slapd): `ldap://localhost:389` (not mapped to the host)
- LDAPS (for legacy apps / direct binds): `ldaps://<host>:636` (TLS)
### Available environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LDAP_BASE_DN` | `dc=example,dc=com` | slapd suffix + app user/group base |
| `LDAP_DOMAIN` | derived from `LDAP_BASE_DN` | DNS domain; default for `LDAP_CERT_CN` and OAuth issuer |
| `LDAP_ADMIN_PASS` | `admin` | slapd root password + app bind password |
| `ORG_NAME` | `SSO Manager` | org name in UI/email/group descriptions |
| `JWT_SECRET` | auto-generated | OAuth JWT signing secret (persist it!) |
| `OAUTH_ISSUER` | `https://sso.<LDAP_DOMAIN>` | OIDC issuer in the discovery doc (browser-facing URL) |
| `LDAP_CERT_CN` | `LDAP_DOMAIN` | CN/SAN on the LDAPS cert (hostname clients verify against) |
| `LDAP_CERT_DIR` | `/etc/openldap/certs` | where the entrypoint looks for `ldap.crt`+`ldap.key` (mount your own here) |
| `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM` | localhost / 587 / empty | outbound email |
| `PORT` | `3001` | host port mapped to the UI |
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
| `LDAP_PORT` | `389` | uncomment the host mapping in compose to expose plain LDAP (not recommended) |
Any `app_*` var may also be set directly to override any config value (see the
table at the top).
### LDAP TLS (LDAPS / StartTLS)
The bundled slapd generates a **self-signed cert** on first start (CN = `LDAP_CERT_CN`,
valid 10 years, SAN includes the CN + `localhost` + `127.0.0.1`) and listens on
`ldaps:///` (636) plus offers StartTLS on `ldap:///` (389). The cert is stored on the
`ldap-certs` volume so it persists across container recreation — clients don't need
to re-trust on every rebuild.
- **Trusting the self-signed cert** (clients): copy `/etc/openldap/certs/ldap.crt`
out of the container and add it to the client's trusted CA store, or set
`TLS_REQCERT never` for quick-and-dirty LAN use. Fetch it with:
```bash
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
```
- **Use your own cert** (CA-signed / internal CA): replace the `ldap-certs` named
volume with a bind mount containing your own `ldap.crt` + `ldap.key`:
```yaml
volumes:
- ./certs:/etc/openldap/certs # must contain ldap.crt + ldap.key
```
The entrypoint leaves existing certs untouched (idempotent).
> Port 389 (plain LDAP) is **not** mapped to the host by default, to avoid cleartext
> password binds over the LAN. Direct-LDAP clients should use LDAPS (636) or
> StartTLS. Uncomment the `389` mapping in `docker-compose.yml` only if you need
> plain LAN binds and accept the risk.
### Fronting with a reverse proxy (theta42/proxy)
The SSO Manager runs HTTP inside the container; terminate TLS at a front proxy.
The [`theta42/proxy`](https://github.com/theta42/proxy) is an OIDC-protected reverse
proxy and a natural fit — it's both an **OIDC client** of the SSO Manager *and* a
**direct LDAP client** for user lookups. To run both together:
1. **Put them on one Docker network** so the proxy can reach the SSO Manager
internally at `http://sso-manager:3001` for token/userinfo (server-to-server),
without exposing the SSO Manager's HTTP port to the internet:
```yaml
# in the proxy's compose, or a shared external network:
networks:
- sso-net
```
2. **Set the SSO's `OAUTH_ISSUER`** to the *browser-facing* HTTPS URL the proxy
serves the SSO at (e.g. `https://sso.yourdomain.com`). The proxy's
`oidc.issuer`/endpoints must match — it can get them from the SSO's
`/.well-known/openid-configuration`. Server-to-server calls from the proxy go to
the internal `http://sso-manager:3001` URL; only the issuer/redirect URLs must
be public.
3. **Register the proxy as an OAuth/OIDC client** in the SSO Manager UI, with a
`redirectUri` matching the proxy's callback (e.g.
`https://proxy.yourdomain.com/api/auth/oidc/callback`), and put the client
secret in the proxy's `secrets.js`.
4. **LDAP for the proxy**: point the proxy's `ldap.url` at
`ldaps://sso-manager:636` (TLS, same Docker network) rather than a LAN IP, and
create a dedicated LDAP service account under `ou=people` (e.g.
`cn=ldapclient,ou=people,…`) via the SSO Manager UI — don't reuse the admin DN.
### Backups (small-business / ~100 users)
- **LDAP data** lives on the `ldap-data` volume (`/var/lib/ldap` in the container).
Back up the directory with an `ldapsearch`/`slapcat` export on a schedule:
```bash
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
```
(Restorable with `ldapadd`/`ldapmodify` against a fresh instance.)
- **Redis** is in-memory and not persisted by default (session/cache only — safe
to lose). If you want session durability, mount a Redis AOF/RDB volume and
enable persistence in `docker-entrypoint.sh`.
- **JWT_SECRET** and **LDAP_ADMIN_PASS** are operational secrets — store them
outside the container (your `.env`, a password manager, etc.).
---
## Method 2: Bare metal (Debian/Ubuntu)
`install.sh` is an idempotent installer: it installs Node.js 20.x, installs and
configures OpenLDAP (modules + overlays + custom schema + directory tree +
required groups), deploys the app to `/opt/sso-manager`, and creates a systemd
unit. Configuration is written to `/opt/sso-manager/conf/secrets.js` (file-based).
### Prerequisites
- Debian 11+ / Ubuntu 20.04+
- Root (`sudo`)
- Internet access
### Install
```bash
sudo ./install.sh \
-p 'your-ldap-password' \
-b 'dc=yourdomain,dc=com' \
-n 'Your Org' \
-o 3001
```
| Flag | Env var | Description |
|------|---------|-------------|
| `-p, --admin-pass` | `LDAP_ADMIN_PASS` | LDAP admin password (required) |
| `-b, --base-dn` | `LDAP_BASE_DN` | Base DN (default `dc=example,dc=com`) |
| `-n, --org-name` | `ORG_NAME` | Org name (default `SSO Manager`) |
| `-o, --port` | `PORT` | HTTP port (default `3001`) |
| `-j, --jwt-secret` | `JWT_SECRET` | JWT secret (default auto-generated) |
| `-s, --smtp-config` | `SMTP_*` | SMTP as `host:port:user:pass` |
| `--skip-ldap` | `SKIP_LDAP` | Skip LDAP setup (use existing) |
| `--skip-app` | `SKIP_APP` | LDAP setup only |
| `--dry-run` | `DRY_RUN` | Show actions without making changes |
### Post-install
```bash
sudo systemctl enable --now sso-manager
journalctl -fu sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
```
### What `install.sh` does
1. Installs Node.js 20.x (NodeSource).
2. Installs OpenLDAP (`slapd`) with: `pw-sha2`, `ppolicy`, `memberof`, `refint`
modules + overlays; the custom `theta42Person` schema (`dateOfBirth`); indexes;
`ou=people`/`ou=groups`/`ou=policies`; a default `pwdPolicy`; and the SSO groups.
3. Installs the app to `/opt/sso-manager` and runs `npm ci --omit=dev`.
4. Generates `conf/secrets.js` (LDAP/SMTP/JWT) and `conf/base.js` (generic defaults).
5. Installs `sso-manager.service` (systemd), enabled on boot.
> For an existing LDAP server, run `sudo ./install.sh --skip-ldap …` and point the
> app at it. For LDAP-only setup on a host that already runs the app elsewhere, use
> `--skip-app`. To (re)configure overlays on an already-installed slapd, prefer
> `ops/ldap-setup.sh` (idempotent, auto-detects the user database).
---
## LDAP requirements (for any external LDAP server)
The app needs these on the LDAP server:
- **Modules:** `pw-sha2` (the app stores user passwords as `{SSHA512}`), `ppolicy`,
`memberof`, `refint`.
- **Custom schema:** the `theta42Person` auxiliary objectClass with `dateOfBirth`
(OID `1.3.6.1.4.1.99999.x`) — see `ops/ldap-setup.sh` for the LDIF.
- **Directory tree:** `ou=people`, `ou=groups`, `ou=policies` under the base DN, a
default `pwdPolicy` at `cn=ppolicy,ou=policies,<base>`.
- **Required groups:** `app_sso_admin` (full admin), `app_sso_invite` (invitation
management), `app_sso_oauth_admin` (OAuth client management).
`ops/ldap-setup.sh -p <admin-password>` configures all of the above idempotently
against a running slapd (auto-detects the database holding your base DN, and
verifies `pwdAccountLockedTime` is live — the attribute the app's
active/inactive toggle depends on).
---
## Migrating an existing instance to the generic defaults
The committed `nodejs/conf/base.js` now ships **generic** defaults
(`dc=example,dc=com`, `localhost`, `SSO Manager`). Previously it carried
Theta42-specific values (LDAP bind DN/bases, SMTP host/user/sender, OAuth issuer).
If you run an existing instance off this repo:
- Move those per-deployment, non-secret values (bind DN, user/group bases, SMTP
host/user/sender, OAuth issuer, org name) from `base.js` into your gitignored
`conf/secrets.js`, **or** set them as `app_*` env vars. Secret values (LDAP bind
password, SMTP password, JWT secret) already belong in `secrets.js`.
- After the change, verify the merged config: `node -e "console.log(require('@simpleworkjs/conf'))"` from the `nodejs/` directory.
---
## Troubleshooting
### `503 OpenLDAP ppolicy overlay is not configured`
The ppolicy overlay isn't attached to the database holding your users, so the
active/inactive toggle can't set `pwdAccountLockedTime`. Run:
```bash
sudo ./ops/ldap-setup.sh -p 'admin-password' -b dc=yourdomain,dc=com
```
### App starts but LDAP operations 401 / "Invalid Credentials"
Check the merged LDAP config the app actually sees:
```bash
cd nodejs && node -e "console.log(require('@simpleworkjs/conf').ldap)"
```
Confirm `url`/`bindDN`/`bindPassword`/`userBase` match your directory. Remember
`app_*` env vars override `secrets.js` which overrides `base.js`.
### `app_*` env vars seem to do nothing
You're on `@simpleworkjs/conf` 1.0.0. Bump to 1.1.0+:
```bash
cd nodejs && npm install @simpleworkjs/conf@^1.1.0
```
### LDAP connection refused
```bash
docker compose exec sso-manager sh -c 'ldapsearch -x -H ldap://localhost:389 -b "" -s base'
systemctl status slapd # bare metal
netstat -tlnp | grep 389
```
---
## Security notes
1. **Never commit `secrets.js`** — it's in `.gitignore`.
2. **Use LDAPS / StartTLS** for any LDAP connection that crosses the network. The
bundled slapd listens on `ldaps:///` (636, TLS) and `ldap:///` (389, plain +
StartTLS); port 389 is not mapped to the host by default so LAN clients can't
bind in cleartext. Direct-LDAP apps (legacy services, `theta42/proxy`) should
use `ldaps://…:636` or StartTLS.
3. **Persist `JWT_SECRET`** — if the Docker image auto-generates one and you don't
set `JWT_SECRET`, issued tokens invalidate on container recreation.
4. **Don't expose the UI's HTTP port to the internet** — terminate TLS at a front
proxy and keep `3001` on the Docker network / localhost only.
5. The all-in-one image runs slapd as the `ldap` user but the app process as root
(matches the bare-metal systemd unit). Harden the app to a non-root user for
production if needed.
+87
View File
@@ -0,0 +1,87 @@
# Theta42 SSO Manager with OpenLDAP - All-in-One Dockerfile
# App + OpenLDAP in a single container, for development/testing or a
# self-contained single-node deployment. For production, run a dedicated
# LDAP server and configure the app via app_* env vars / mounted secrets.js.
FROM node:20-alpine
# Install OpenLDAP and required packages.
# Alpine splits OpenLDAP into many small subpackages; there is no catch-all
# "openldap-overlays" package. We install exactly the backends/overlays/modules
# the app depends on:
# openldap-back-mdb : the mdb backend (slapd.conf uses `database mdb`)
# openldap-overlay-ppolicy : ppolicy module + overlay (account locking)
# openldap-overlay-memberof : reverse group membership
# openldap-overlay-refint : referential integrity on group members
# openldap-passwd-sha2 : pw-sha2 module ({SSHA512} user password hashing)
# Note: Alpine does NOT ship a ppolicy.schema file — on OpenLDAP 2.6 the ppolicy
# schema is built into ppolicy.so and registered when the module loads, so
# docker-entrypoint.sh loads it via `moduleload ppolicy` (no schema include).
# openssl : used by docker-entrypoint.sh to generate a JWT secret
RUN apk add --no-cache \
openldap \
openldap-clients \
openldap-back-mdb \
openldap-overlay-ppolicy \
openldap-overlay-memberof \
openldap-overlay-refint \
openldap-passwd-sha2 \
dumb-init \
bash \
openssl \
redis \
&& rm -rf /var/cache/apk/*
# The openldap package already creates the `ldap` user/group, which slapd runs
# as (see -u ldap -g ldap in docker-entrypoint.sh). Nothing to add here.
WORKDIR /app
# Create required directories. slapd runs as the ldap user; the app process
# runs as root in this image (matches the bare-metal systemd unit).
RUN mkdir -p /var/lib/ldap /etc/ldap/sasl2 && \
chown -R ldap:ldap /var/lib/ldap /etc/ldap/sasl2
# Copy application source and install production dependencies.
# .dockerignore excludes nodejs/node_modules so npm ci builds a clean tree.
COPY nodejs/package*.json ./
RUN npm ci --omit=dev
COPY nodejs/app.js ./
COPY nodejs/bin ./bin
COPY nodejs/conf ./conf
COPY nodejs/controller ./controller
COPY nodejs/middleware ./middleware
COPY nodejs/models ./models
COPY nodejs/routes ./routes
COPY nodejs/utils ./utils
COPY nodejs/views ./views
COPY nodejs/public ./public
# routes/index.js reads path.join(__dirname, '../../tos.md') at boot. With the
# app flattened into /app, __dirname is /app/routes and ../../ resolves to /,
# so the file must exist at /tos.md (mirroring the repo where tos.md sits one
# level above the nodejs/ app dir). Without this the app crashes on startup.
COPY tos.md /tos.md
# Copy startup script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Expose ports
# 3001: SSO Manager web interface (HTTP — terminate TLS at the front proxy)
# 389: LDAP (plain + StartTLS) — used internally by the app; map to host only
# if you want LAN clients to bind without TLS (not recommended).
# 636: LDAPS — for legacy apps / direct LDAP binds over the network (TLS)
EXPOSE 3001 389 636
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# dumb-init reaps zombies and forwards signals to the node process the
# entrypoint execs into. Without it SIGTERM from `docker stop` is ignored
# and the container hits the 10s kill timeout.
ENTRYPOINT ["dumb-init", "/usr/local/bin/docker-entrypoint.sh"]
CMD ["node", "bin/www"]
+93
View File
@@ -0,0 +1,93 @@
# Docker Compose for the SSO Manager all-in-one image (app + OpenLDAP in one container).
#
# The image (Dockerfile.openldap) bundles a slapd that the app talks to over
# localhost. Configuration is supplied to the app via `app_*` environment
# variables — the highest-precedence config layer in @simpleworkjs/conf. Any
# `app_*` var not set here falls back to a sensible default baked into
# docker-entrypoint.sh (e.g. app_ldap__url -> ldap://localhost:389).
#
# Copy this file to .env (or export the vars) to set the secrets below, or
# set them inline on the command line:
# LDAP_ADMIN_PASS=... JWT_SECRET=... docker compose up -d
#
# Requires @simpleworkjs/conf >= 1.1.0 in the image (env overrides). Refresh
# nodejs/package-lock.json with `npm install @simpleworkjs/conf@^1.1.0` before
# building, so the image actually contains the env-override feature.
services:
sso-manager:
build:
context: .
dockerfile: Dockerfile.openldap
container_name: sso-manager
restart: unless-stopped
ports:
# SSO Manager web UI (HTTP inside the container; terminate TLS at the
# front proxy — e.g. the theta42/proxy). Don't expose 3001 to the open
# internet; bind it to localhost or leave it on the docker network only.
- "${PORT:-3001}:3001"
# LDAPS — direct LDAP binds from legacy apps / the proxy over the network
# (TLS, self-signed cert by default; mount your own at LDAP_CERT_DIR).
- "${LDAPS_PORT:-636}:636"
# LDAP plain (389) is NOT mapped to the host by default — it would allow
# cleartext password binds over the LAN. Uncomment to permit StartTLS or
# plain binds from the LAN (not recommended):
# - "${LDAP_PORT:-389}:389"
environment:
# ── LDAP server-side (configures the bundled slapd) ──
- LDAP_BASE_DN=${LDAP_BASE_DN:-dc=example,dc=com}
- LDAP_DOMAIN=${LDAP_DOMAIN:-example.com}
- LDAP_ADMIN_PASS=${LDAP_ADMIN_PASS:-admin} # slapd root + app bind password
- ORG_NAME=${ORG_NAME:-SSO Manager}
# CN on the LDAP TLS cert. Clients verify against this hostname. Leave
# empty to default to LDAP_DOMAIN; set to the public hostname clients
# connect over (computed in docker-entrypoint.sh, not here, to avoid
# nested-variable interpolation limitations in compose v1).
- LDAP_CERT_CN=${LDAP_CERT_CN:-}
# ── App config overrides (app_* -> @simpleworkjs/conf) ──
# Defaults come from docker-entrypoint.sh; set these to override.
# - app_oauth__issuer=https://sso.example.com
# - app_ldap__url=ldap://localhost:389 # default; points at bundled slapd
# - app_ldap__bindDN=cn=admin,${LDAP_BASE_DN}
# - app_ldap__bindPassword=${LDAP_ADMIN_PASS}
# - app_ldap__userBase=ou=people,${LDAP_BASE_DN}
# - app_ldap__groupBase=ou=groups,${LDAP_BASE_DN}
- app_oauth__jwtSecret=${JWT_SECRET} # falls back to an auto-generated secret
# OIDC issuer advertised in /.well-known/openid-configuration. This is the
# browser-facing URL the front proxy serves the SSO at — the theta42/proxy
# (an OIDC client) and other apps use it for discovery. Server-to-server
# token/userinfo calls from the proxy can go to http://sso-manager:3001
# over the docker network; only the issuer/redirect URLs must be public.
# Leave empty to default to https://sso.<LDAP_DOMAIN> (set in the entrypoint).
- app_oauth__issuer=${OAUTH_ISSUER:-}
- app_name=${ORG_NAME:-SSO Manager}
# ── Optional SMTP (outbound email) ──
- app_smtp__host=${SMTP_HOST:-localhost}
- app_smtp__port=${SMTP_PORT:-587}
- app_smtp__user=${SMTP_USER:-}
- app_smtp__pass=${SMTP_PASS:-}
- app_smtp__from=${SMTP_FROM:-SSO Manager <noreply@example.com>}
- NODE_ENV=production
- NODE_PORT=3001
volumes:
# Persist the LDAP database across container recreation.
- ldap-data:/var/lib/ldap
# Persist the auto-generated self-signed TLS cert so clients don't have to
# re-trust it on every rebuild. To use your own CA-signed cert instead,
# replace this with a bind mount of your cert dir, e.g.:
# - ./certs:/etc/openldap/certs
# (must contain ldap.crt + ldap.key; the entrypoint leaves them untouched).
- ldap-certs:/etc/openldap/certs
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
ldap-data:
ldap-certs:
+312
View File
@@ -0,0 +1,312 @@
#!/usr/bin/env bash
# docker-entrypoint.sh — initialize the bundled OpenLDAP and start SSO Manager.
# Used by Dockerfile.openldap (the all-in-one image: app + slapd in one container).
#
# This is intended for development/testing / single-node deployments. For
# production, run a dedicated LDAP server and point the app at it via app_*
# env vars (or a mounted conf/secrets.js) using the app-only image.
#
# The app reads its configuration from conf/base.js + conf/secrets.js, deep-merged
# by @simpleworkjs/conf, with `app_*` environment variables as the
# highest-precedence override layer. This entrypoint exports those `app_*`
# vars so the app connects to the bundled slapd without any mounted secrets
# file. Any `app_*` var already set in the environment wins (the values below
# are defaults/fallbacks only).
set -e
# ── LDAP server-side configuration ──────────────────────────────────────────
LDAP_BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
LDAP_ADMIN_PASS="${LDAP_ADMIN_PASS:-admin}"
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) unless given.
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
LDAP_DOMAIN=$(echo "$LDAP_BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
fi
ORG_NAME="${ORG_NAME:-SSO Manager}"
LDAP_BIND_DN="${LDAP_BIND_DN:-cn=admin,${LDAP_BASE_DN}}"
# The app (inside the container) talks to the local slapd over localhost.
APP_LDAP_URL="${app_ldap__url:-ldap://localhost:389}"
info() { echo "[INFO] $*"; }
error() { echo "[ERROR] $*" >&2; }
# ── Locate the OpenLDAP module directory ────────────────────────────────────
# slapd.conf needs `modulepath` to find pw-sha2/ppolicy/memberof/refint. The
# path varies by distro; auto-detect rather than hardcode.
MODULE_PATH=""
for p in /usr/lib/openldap /usr/lib/ldap /usr/local/lib/openldap /opt/local/lib/openldap; do
if [[ -d "$p" ]]; then MODULE_PATH="$p"; break; fi
done
# ── TLS certificate for LDAPS / StartTLS ────────────────────────────────────
# Legacy apps (e.g. the theta42/proxy, Gitea, Emby) bind to LDAP directly over the
# network. To keep password binds off the wire in cleartext we expose LDAPS
# (636) and allow StartTLS on 389. By default a self-signed cert is generated on
# first start; mount your own cert+key at LDAP_CERT_DIR to use a CA-signed cert
# instead (idempotent: existing certs are never overwritten).
LDAP_CERT_DIR="${LDAP_CERT_DIR:-/etc/openldap/certs}"
# CN/SAN hostname clients will verify against. Default to the DNS domain derived
# from the base DN (dc=foo,dc=bar -> foo.bar); override for a public hostname.
LDAP_CERT_CN="${LDAP_CERT_CN:-${LDAP_DOMAIN:-localhost}}"
mkdir -p "$LDAP_CERT_DIR"
if [[ -f "$LDAP_CERT_DIR/ldap.crt" && -f "$LDAP_CERT_DIR/ldap.key" ]]; then
info "Using existing LDAP TLS cert at $LDAP_CERT_DIR (mounted or previously generated)"
else
info "Generating self-signed LDAP TLS cert (CN=$LDAP_CERT_CN, valid 10y)..."
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout "$LDAP_CERT_DIR/ldap.key" -out "$LDAP_CERT_DIR/ldap.crt" \
-days 3650 -subj "/CN=$LDAP_CERT_CN" \
-addext "subjectAltName=DNS:$LDAP_CERT_CN,DNS:localhost,IP:127.0.0.1" \
>/dev/null 2>&1 || {
error "Failed to generate LDAP TLS cert"
exit 1
}
fi
# slapd runs as the ldap user and must read both cert and key.
chown -R ldap:ldap "$LDAP_CERT_DIR" 2>/dev/null || true
chmod 600 "$LDAP_CERT_DIR/ldap.key" 2>/dev/null || true
chmod 644 "$LDAP_CERT_DIR/ldap.crt" 2>/dev/null || true
# ── Generate slapd.conf ─────────────────────────────────────────────────────
# We use slapd.conf (static config) rather than cn=config so the whole directory
# is configured from a single generated file. slapd is started with -f below so
# it actually reads this file (the original entrypoint omitted -f, so slapd
# ignored it entirely).
cat > /etc/openldap/slapd.conf << SLAPDEOF
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
# Module loading (pw-sha2 provides {SSHA512} used by the app for user passwords;
# ppolicy/memberof/refint are the overlays the app depends on). On OpenLDAP 2.5+
# the ppolicy schema (pwdPolicy, pwdAccountLockedTime, ...) is built into
# ppolicy.so and registered when the module loads — there is no separate
# ppolicy.schema to include, and loading the module before the overlay below
# is what makes the pwdPolicy objectClass known to slapd.
SLAPMODULEPATH
moduleload back_mdb
moduleload pw-sha2
moduleload ppolicy
moduleload memberof
moduleload refint
# TLS (LDAPS on 636 + StartTLS on 389). Cert/key paths are fixed; the files are
# generated/mounted above. We accept clients without their own cert (the common
# case for LDAP bind clients) and treat our own self-signed cert as the CA.
TLSCertificateFile /etc/openldap/certs/ldap.crt
TLSCertificateKeyFile /etc/openldap/certs/ldap.key
TLSCACertificateFile /etc/openldap/certs/ldap.crt
TLSVerifyClient never
# Database configuration
database mdb
# Store the mdb files on the persistent volume (docker-compose mounts a named
# volume at /var/lib/ldap). Without this, mdb defaults to
# /var/lib/openldap/openldap-data and the directory would be lost on recreation.
directory /var/lib/ldap
suffix SUFFIX_PLACEHOLDER
rootdn BIND_DN_PLACEHOLDER
# rootpw uses {SSHA} (built into slapd, no module needed) so slappasswd never
# depends on a loadable module to generate or verify it. User passwords are
# stored as {SSHA512} by the app itself and verified via the pw-sha2 module.
rootpw ROOTPW_PLACEHOLDER
# Indexes
index objectClass eq
index uid eq,sub
index mail eq,sub
index cn eq,sub
index member eq
index uidNumber eq
index gidNumber eq
# ppolicy overlay (account locking — the app's active/inactive toggle relies on
# pwdAccountLockedTime being a known attribute).
overlay ppolicy
ppolicy_default "cn=ppolicy,ou=policies,SUFFIX_PLACEHOLDER"
ppolicy_use_lockout true
# memberof overlay (reverse group membership)
overlay memberof
memberof-group-oc groupOfNames
memberof-member-ad member
memberof-memberof-ad memberOf
# refint overlay (referential integrity on group membership)
overlay refint
refint_attributes memberOf member manager owner
# Access controls
access to attrs=userPassword
by dn="BIND_DN_PLACEHOLDER" write
by anonymous auth
by self write
by * none
access to *
by dn="BIND_DN_PLACEHOLDER" write
by * read
SLAPDEOF
# Generate the rootpw hash ({SSHA}, built-in — no module dependency).
HASHED_PASS=$(slappasswd -s "$LDAP_ADMIN_PASS")
# Fill placeholders.
DC_VALUE="${LDAP_BASE_DN%%,*}" # dc=example
DC_VALUE="${DC_VALUE#dc=}" # example
sed -i "s|SUFFIX_PLACEHOLDER|${LDAP_BASE_DN}|g" /etc/openldap/slapd.conf
sed -i "s|BIND_DN_PLACEHOLDER|${LDAP_BIND_DN}|g" /etc/openldap/slapd.conf
sed -i "s|ROOTPW_PLACEHOLDER|${HASHED_PASS}|g" /etc/openldap/slapd.conf
if [[ -n "$MODULE_PATH" ]]; then
sed -i "s|^SLAPMODULEPATH$|modulepath ${MODULE_PATH}|" /etc/openldap/slapd.conf
else
sed -i "/^SLAPMODULEPATH$/d" /etc/openldap/slapd.conf
fi
chown ldap:ldap /etc/openldap/slapd.conf 2>/dev/null || true
chown -R ldap:ldap /var/lib/ldap 2>/dev/null || true
# ── Start slapd ─────────────────────────────────────────────────────────────
info "Starting OpenLDAP (base DN: ${LDAP_BASE_DN})..."
# -f forces slapd to use our generated slapd.conf (not cn=config).
# -h listens on ldap:/// (389: plain + StartTLS) and ldaps:/// (636: LDAPS).
# ldapi:/// is intentionally omitted: its default socket dir doesn't exist on
# Alpine and the container only uses simple bind over ldap://localhost:389.
slapd -d 0 -u ldap -g ldap -f /etc/openldap/slapd.conf -h "ldap:/// ldaps:///" &
SLAPD_PID=$!
# Wait for slapd to answer the root DSE (means it's up, regardless of DB state).
for i in $(seq 1 30); do
if ldapsearch -x -H ldap://localhost:389 -b "" -s base "(objectClass=*)" >/dev/null 2>&1; then
info "OpenLDAP is ready"
break
fi
info "Waiting for OpenLDAP... ($i/30)"
sleep 1
done
if ! kill -0 "$SLAPD_PID" 2>/dev/null; then
error "OpenLDAP failed to start"
exit 1
fi
# ── Initialize the directory tree (idempotent) ──────────────────────────────
# Only seed if the base DN doesn't exist yet, so restarting the container is safe.
if ! ldapsearch -x -H ldap://localhost:389 -b "$LDAP_BASE_DN" -s base "(objectClass=*)" >/dev/null 2>&1; then
info "Initializing LDAP directory..."
ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 << EOF
dn: ${LDAP_BASE_DN}
objectClass: dcObject
objectClass: organization
dc: ${DC_VALUE}
o: ${ORG_NAME}
dn: ou=people,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: people
dn: ou=groups,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: groups
dn: ou=policies,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: policies
dn: cn=ppolicy,ou=policies,${LDAP_BASE_DN}
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: ppolicy
pwdAttribute: 2.5.4.35
pwdLockout: FALSE
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
EOF
# Required SSO groups. The app gates admin/invite/oauth-admin on these.
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
ldapadd -x -D "$LDAP_BIND_DN" -w "$LDAP_ADMIN_PASS" -H ldap://localhost:389 << EOF || true
dn: cn=${group},ou=groups,${LDAP_BASE_DN}
objectClass: groupOfNames
objectClass: top
cn: ${group}
description: ${ORG_NAME} ${group} group
member: ${LDAP_BIND_DN}
EOF
done
info "LDAP directory initialized"
else
info "LDAP directory already initialized — skipping seed"
fi
# ── Start Redis ──────────────────────────────────────────────────────────────
# The app stores models/sessions in Redis (model-redis). The all-in-one image
# bundles a Redis server for a self-contained single-node deployment. Point the
# app at an external Redis instead by setting app_redis__host before starting.
# In-memory, no persistence: cache/session data is rebuilt on restart.
if [[ -z "${app_redis__host:-}" ]]; then
info "Starting Redis..."
redis-server --save "" --appendonly no --daemonize no &
REDIS_PID=$!
for i in $(seq 1 15); do
if redis-cli ping >/dev/null 2>&1; then
info "Redis is ready"
break
fi
sleep 0.5
done
if ! kill -0 "$REDIS_PID" 2>/dev/null; then
error "Redis failed to start"
exit 1
fi
# App defaults to 127.0.0.1:6379 (the redis client default), so no override
# is needed when running the bundled Redis.
fi
# ── Generate a JWT secret if none was provided ──────────────────────────────
if [[ -z "${JWT_SECRET:-}" && -z "${app_oauth__jwtSecret:-}" ]]; then
if command -v openssl >/dev/null 2>&1; then
JWT_SECRET=$(openssl rand -hex 32)
else
JWT_SECRET="$LDAP_ADMIN_PASS-jwt-secret-$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' ')"
fi
info "Generated JWT secret (set JWT_SECRET or app_oauth__jwtSecret to persist)"
fi
# ── Export app_* config overrides for the SSO Manager process ────────────────
# These are the highest-precedence config layer in @simpleworkjs/conf. Any
# value already set in the environment is preserved (${VAR:-default}).
export app_ldap__url="${app_ldap__url:-$APP_LDAP_URL}"
export app_ldap__bindDN="${app_ldap__bindDN:-$LDAP_BIND_DN}"
export app_ldap__bindPassword="${app_ldap__bindPassword:-$LDAP_ADMIN_PASS}"
export app_ldap__userBase="${app_ldap__userBase:-ou=people,${LDAP_BASE_DN}}"
export app_ldap__groupBase="${app_ldap__groupBase:-ou=groups,${LDAP_BASE_DN}}"
export app_oauth__jwtSecret="${app_oauth__jwtSecret:-$JWT_SECRET}"
# OIDC issuer advertised in /.well-known/openid-configuration. Default to the
# public https URL on the SSO subdomain of the LDAP domain; override with
# OAUTH_ISSUER / app_oauth__issuer. Computed here (not in compose) because
# compose v1 doesn't interpolate nested ${VAR:-...} defaults.
export app_oauth__issuer="${app_oauth__issuer:-https://sso.${LDAP_DOMAIN}}"
export app_name="${app_name:-$ORG_NAME}"
# SMTP (optional). If no user/pass, disable auth by clearing the user.
export app_smtp__host="${app_smtp__host:-${SMTP_HOST:-localhost}}"
export app_smtp__port="${app_smtp__port:-${SMTP_PORT:-587}}"
if [[ -n "${SMTP_USER:-}" || -n "${SMTP_PASS:-}" ]]; then
export app_smtp__user="${app_smtp__user:-$SMTP_USER}"
export app_smtp__pass="${app_smtp__pass:-$SMTP_PASS}"
else
export app_smtp__user="${app_smtp__user:-}"
fi
# HTTP port for the app (bin/www reads NODE_PORT).
export NODE_PORT="${NODE_PORT:-${PORT:-3001}}"
export NODE_ENV="${NODE_ENV:-production}"
info "Starting SSO Manager on port ${NODE_PORT}..."
exec "$@"
+9
View File
@@ -0,0 +1,9 @@
title: SSO Manager
description: A self-hosted OpenID Connect provider with an OpenLDAP directory and a web management UI
theme: jekyll-theme-cayman
show_downloads: true
github:
repository_url: https://github.com/theta42/sso-manager-node
zip_url: https://github.com/theta42/sso-manager-node/archive/refs/heads/master.zip
tar_url: https://github.com/theta42/sso-manager-node/archive/refs/heads/master.tar.gz
repository_name: theta42/sso-manager-node
+99
View File
@@ -0,0 +1,99 @@
---
layout: default
title: Configuration
---
# Configuration
[← Back to Home](index.html)
The app loads configuration via
[`@simpleworkjs/conf`](https://www.npmjs.com/package/@simpleworkjs/conf), which
deep-merges, in order (later wins):
1. `conf/base.js` — committed, generic defaults (`dc=example,dc=com`,
`localhost`, `SSO Manager`).
2. `conf/<NODE_ENV>.js` — optional, environment-specific.
3. `conf/secrets.js` — gitignored; secrets + per-deployment values.
4. **`app_*` environment variables** — the highest-precedence layer.
Any env var whose name starts with `app_` overrides the merged config. The rest
of the name splits on **double-underscore** (`__`) into a nested path. Values are
`JSON.parse`-coerced when possible (numbers, booleans, null, JSON) and kept as
raw strings otherwise.
## Examples
| Env var | Sets | Type |
|---------|------|------|
| `app_ldap__url=ldap://host:389` | `conf.ldap.url` | string |
| `app_ldap__bindPassword=secret` | `conf.ldap.bindPassword` | string |
| `app_ldap__userBase=ou=people,dc=…` | `conf.ldap.userBase` | string |
| `app_oauth__jwtSecret=...` | `conf.oauth.jwtSecret` | string |
| `app_oauth__issuer=https://sso.example.com` | `conf.oauth.issuer` | string |
| `app_oauth__token_lifetime__access_token=3600` | `conf.oauth.token_lifetime.access_token` | number |
| `app_smtp__secure=false` | `conf.smtp.secure` | boolean |
| `app_smtp__host=smtp.example.com` | `conf.smtp.host` | string |
| `app_name=My SSO` | `conf.name` | string |
| `app_redis__host=redis.local` | `conf.redis.host` | string (external Redis) |
## The `app_*` env layer requires conf >= 1.1.0
The `app_*` environment-variable override layer was added in
`@simpleworkjs/conf` **1.1.0**. On 1.0.0 the app ignores all `app_*` vars and only
reads `base.js` / `<NODE_ENV>.js` / `secrets.js`. The Docker image will not honor
`app_*` env on 1.0.0. Refresh the lock from the `nodejs/` directory:
```bash
cd nodejs && npm install @simpleworkjs/conf@^1.1.0
```
## Inspecting the merged config
From the `nodejs/` directory:
```bash
node -e "console.log(require('@simpleworkjs/conf').ldap)"
node -e "console.log(require('@simpleworkjs/conf').oauth)"
node -e "console.log(require('@simpleworkjs/conf'))" # everything
```
Or, inside the running container:
```bash
docker compose exec sso-manager node -e "console.log(require('@simpleworkjs/conf').ldap)"
```
`app_*` env vars override `secrets.js`, which overrides `base.js` — if a value
isn't what you expect, check those layers in that order.
## Migrating an existing instance to the generic defaults
The committed `nodejs/conf/base.js` ships **generic** defaults
(`dc=example,dc=com`, `localhost`, `SSO Manager`). Previously it carried
Theta42-specific values (LDAP bind DN/bases, SMTP host/user/sender, OAuth
issuer). If you run an existing instance off this repo:
- Move per-deployment, non-secret values (bind DN, user/group bases, SMTP
host/user/sender, OAuth issuer, org name) from `base.js` into your gitignored
`conf/secrets.js`, **or** set them as `app_*` env vars.
- Secret values (LDAP bind password, SMTP password, JWT secret) already belong
in `secrets.js`.
## Troubleshooting `app_*` env vars
### `app_*` vars seem to do nothing
You're on `@simpleworkjs/conf` 1.0.0. Bump to 1.1.0+ (above).
### LDAP operations 401 / "Invalid Credentials"
Check the merged LDAP config the app actually sees:
```bash
cd nodejs && node -e "console.log(require('@simpleworkjs/conf').ldap)"
```
Confirm `url` / `bindDN` / `bindPassword` / `userBase` match your directory.
[← Back to Home](index.html)
+177
View File
@@ -0,0 +1,177 @@
---
layout: default
title: Deployment
---
# Deployment Guide
[← Back to Home](index.html)
Two supported methods:
1. **Docker** — a single all-in-one image bundling the app + OpenLDAP + Redis.
2. **Bare metal**`install.sh` on Debian/Ubuntu (Node.js, OpenLDAP, Redis, systemd unit).
## Method 1: Docker (all-in-one)
The image (`Dockerfile.openldap`) bundles OpenLDAP + the app + Redis in one
container. The app connects to the bundled slapd over `localhost:389`
automatically; you only need to set a few secrets.
```bash
# Minimal: an LDAP admin password + a JWT secret, then build + start.
LDAP_ADMIN_PASS='choose-a-strong-password' \
JWT_SECRET="$(openssl rand -hex 32)" \
docker compose up -d --build
```
For a customized deployment, put overrides in a `.env` next to
`docker-compose.yml`:
```env
LDAP_BASE_DN=dc=yourdomain,dc=com
LDAP_DOMAIN=yourdomain.com
LDAP_ADMIN_PASS=your-admin-password
ORG_NAME=Your Org
JWT_SECRET=your-jwt-secret
OAUTH_ISSUER=https://sso.yourdomain.com # browser-facing URL the proxy serves
LDAP_CERT_CN=sso.yourdomain.com # hostname LDAPS clients verify
SMTP_HOST=smtp.yourdomain.com
SMTP_PORT=587
SMTP_USER=noreply@yourdomain.com
SMTP_PASS=your-smtp-password
SMTP_FROM=Your Org <noreply@yourdomain.com>
PORT=3001
LDAPS_PORT=636
```
Then `docker compose up -d --build`.
### Access
- SSO Manager UI: `http://localhost:3001` (HTTP — put a TLS-terminating proxy in front)
- Health: `http://localhost:3001/health``{"status":"ok"}`
- OIDC discovery: `http://localhost:3001/.well-known/openid-configuration`
- LDAPS (legacy apps / direct binds): `ldaps://<host>:636`
### Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `LDAP_BASE_DN` | `dc=example,dc=com` | slapd suffix + app user/group base |
| `LDAP_DOMAIN` | derived from base DN | DNS domain; default for cert CN + issuer |
| `LDAP_ADMIN_PASS` | `admin` | slapd root password + app bind password |
| `ORG_NAME` | `SSO Manager` | org name in UI/email/group descriptions |
| `JWT_SECRET` | auto-generated | OAuth JWT signing secret (**persist it**) |
| `OAUTH_ISSUER` | `https://sso.<LDAP_DOMAIN>` | OIDC issuer (browser-facing URL) |
| `LDAP_CERT_CN` | `LDAP_DOMAIN` | CN/SAN on the LDAPS cert |
| `LDAP_CERT_DIR` | `/etc/openldap/certs` | look for `ldap.crt`+`ldap.key` here (mount your own) |
| `SMTP_HOST`/`SMTP_PORT`/`SMTP_USER`/`SMTP_PASS`/`SMTP_FROM` | localhost / 587 / empty | outbound email |
| `PORT` | `3001` | host port mapped to the UI |
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
Any `app_*` var may also be set directly to override any config value — see
[Configuration](configuration.html).
### LDAP TLS (LDAPS / StartTLS)
The bundled slapd generates a **self-signed cert** on first start (CN =
`LDAP_CERT_CN`, valid 10y, SAN = CN + `localhost` + `127.0.0.1`) and listens on
`ldaps:///` (636) + StartTLS on `ldap:///` (389). The cert lives on the
`ldap-certs` volume so it persists across container recreation.
- **Trust it** (clients): copy the cert out and add it to the client's CA store,
or set `TLS_REQCERT never` for quick LAN use:
```bash
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
```
- **Use your own cert**: replace the `ldap-certs` named volume with a bind mount
containing your `ldap.crt` + `ldap.key`. The entrypoint leaves existing certs
untouched.
> Port 389 (plain LDAP) is **not** mapped to the host by default — direct-LDAP
> clients should use LDAPS (636) or StartTLS.
### Backups (~100 users)
```bash
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
```
Restorable with `ldapadd`/`ldapmodify` against a fresh instance. Redis is
in-memory (session/cache only — safe to lose). Persist `JWT_SECRET` +
`LDAP_ADMIN_PASS` outside the container (your `.env`, a password manager).
## Method 2: Bare metal (Debian/Ubuntu)
`install.sh` is an idempotent installer: Node.js 20.x, OpenLDAP (modules +
overlays + custom schema + directory tree + required groups), the app at
`/opt/sso-manager`, and a systemd unit.
```bash
sudo ./install.sh \
-p 'your-ldap-password' \
-b 'dc=yourdomain,dc=com' \
-n 'Your Org' \
-o 3001
```
| Flag | Description |
|------|-------------|
| `-p, --admin-pass` | LDAP admin password (required) |
| `-b, --base-dn` | Base DN (default `dc=example,dc=com`) |
| `-n, --org-name` | Org name (default `SSO Manager`) |
| `-o, --port` | HTTP port (default `3001`) |
| `-j, --jwt-secret` | JWT secret (default auto-generated) |
| `-s, --smtp-config` | SMTP as `host:port:user:pass` |
| `--skip-ldap` | Skip LDAP setup (use existing) |
| `--skip-app` | LDAP setup only |
| `--dry-run` | Show actions without making changes |
Post-install:
```bash
sudo systemctl enable --now sso-manager
curl http://localhost:3001/health # -> {"status":"ok"}
```
For an existing LDAP server, run `sudo ./install.sh --skip-ldap …` and point the
app at it. To (re)configure overlays on an already-installed slapd, prefer
`ops/ldap-setup.sh` (idempotent, auto-detects the user database).
## Fronting with a reverse proxy
The SSO runs HTTP inside the container; terminate TLS at a front proxy. The
[`theta42/proxy`](https://github.com/theta42/proxy) is an OIDC-protected reverse
proxy and a natural fit — it's **both** an OIDC client of this SSO *and* a
direct LDAP client for user lookups.
1. **One Docker network** so the proxy reaches the SSO internally at
`http://sso-manager:3001` (token/userinfo, server-to-server) without exposing
the SSO's HTTP port.
2. **Set the SSO's `OAUTH_ISSUER`** to the *browser-facing* HTTPS URL the proxy
serves the SSO at (e.g. `https://sso.yourdomain.com`).
3. **Register the proxy as an OIDC client** in the SSO UI, with `redirectUri`
matching the proxy's callback (`https://proxy.yourdomain.com/api/auth/oidc/callback`).
4. **LDAP for the proxy**: point `ldap.url` at `ldaps://sso-manager:636` and
create a dedicated service account under `ou=people` (e.g.
`cn=ldapclient,ou=people,…`) — don't reuse the admin DN.
The [`theta42/theta-env`](https://github.com/theta42/theta-env) unified repo
automates all four steps with `./setup.sh` — see
[theta-env docs](https://theta42.github.io/theta-env/).
## Security notes
1. **Never commit `secrets.js`** — it's in `.gitignore`.
2. **Use LDAPS / StartTLS** for any LDAP that crosses the network. Port 389 is
not mapped to the host by default so LAN clients can't bind in cleartext.
3. **Persist `JWT_SECRET`** — an auto-generated one invalidates all tokens on
container recreation.
4. **Don't expose the UI's HTTP port to the internet** — terminate TLS at a
front proxy and keep `3001` on the Docker network / localhost only.
5. The all-in-one image runs slapd as the `ldap` user but the app as root
(matches the bare-metal unit). Harden to a non-root user for production.
[← Back to Home](index.html)
+105
View File
@@ -0,0 +1,105 @@
---
layout: default
title: Home
---
# 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.
## Features
- **OpenID Connect / OAuth 2.0 provider** — issue your own access/refresh/id
tokens; protect your apps with OIDC login.
- **OpenLDAP directory** — users, groups, POSIX accounts (`posixAccount`/
`inetOrgPerson`), SSH public keys, and sudo roles, with `memberOf` +
referential-integrity overlays.
- **Web management UI** — manage users, groups, and OAuth clients from a
browser; invite/password-reset flows over email.
- **LDAPS for legacy apps** — apps that bind LDAP directly (Gitea, Emby, …)
can use LDAPS (636) / StartTLS.
- **All-in-one Docker image** — app + OpenLDAP + Redis in one container, or
run each piece separately via `app_*` env config.
## Quick Start
### Docker (all-in-one)
```bash
git clone https://github.com/theta42/sso-manager-node.git
cd sso-manager-node
cp secrets.js.example nodejs/conf/secrets.js # edit it, or use app_* env
docker compose up -d --build
```
The web UI comes up at `http://localhost:3001`. See the
[Deployment Guide](deployment.html) for the full set of `app_*` env vars.
### Bare metal (Debian/Ubuntu)
```bash
sudo ./install.sh
```
Idempotent installer — installs Node.js, OpenLDAP, Redis, configures the app,
and starts a systemd unit. Re-run to update.
### Run it together with the proxy
The proxy ([theta42/proxy](https://github.com/theta42/proxy)) fronts this SSO
under TLS and protects it with OIDC, while also binding LDAP directly. Run both
with one command via [theta-env](https://github.com/theta42/theta-env):
```bash
git clone --recursive https://github.com/theta42/theta-env.git
cd theta-env && cp .env.example .env # edit, then:
./setup.sh
```
## Documentation
- [Deployment Guide](deployment.html) — Docker + bare metal, the config layers,
the `app_*` env reference, backups.
- [Configuration](configuration.html) — every `app_*` env var and the conf
merge order.
- [OAuth / OIDC](oauth.html) — the provider: discovery, client management,
token lifetimes, scopes.
- [LDAP](ldap.html) — directory layout, TLS, overlays, schema, direct-bind
service accounts.
## 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 │
└────────────────────────┘
```
## Community
- [GitHub Repository](https://github.com/theta42/sso-manager-node)
- [Issue Tracker](https://github.com/theta42/sso-manager-node/issues)
- [Pull Requests](https://github.com/theta42/sso-manager-node/pulls)
## License
MIT License — see the repository for details.
+153
View File
@@ -0,0 +1,153 @@
---
layout: default
title: LDAP
---
# LDAP Directory
[← Back to Home](index.html)
SSO Manager runs an OpenLDAP directory holding your users and groups. The app
authenticates against it over `localhost:389` (inside the all-in-one container)
and exposes **LDAPS** (`ldaps://…:636`, TLS) for legacy apps that bind LDAP
directly — Gitea, Emby, the theta42/proxy, etc.
## Directory layout
```
dc=yourdomain,dc=com
├── ou=people users (inetOrgPerson + posixAccount + …)
├── ou=groups groups (groupOfNames)
└── ou=policies password policies (pwdPolicy)
└── cn=ppolicy default policy
```
### Users
User entries are `cn=<uid>,ou=people,<base>` and carry the objectClasses:
- `inetOrgPerson` (cn, sn, mail, …) — identity / contact attrs.
- `posixAccount` (uid, uidNumber, gidNumber, homeDirectory) — the SSO's
`userFilter` is `(objectClass=posixAccount)`, so a user is "a real account"
iff it has `posixAccount`.
- `ldapPublicKey` — SSH public keys (`sshPublicKey`).
- `sudoRole` — per-user sudo rules (`sudoCommand`, `sudoHost`, `sudoUser`).
- `theta42Person` (custom auxiliary; `dateOfBirth`).
Passwords are stored as `{SSHA512}` (8-byte salt, sha512(pass+salt), base64),
verified by the `pw-sha2` module. The app's `hashPasswordSSHA512` is the
canonical hasher; if you provision users out-of-band, hash passwords the same
way or use `slappasswd -h '{SSHA512}'`.
### Groups
Groups are `cn=<name>,ou=groups,<base>` (`groupOfNames`) with a `member`
attribute listing member DNs. The `memberOf` overlay populates reverse
membership (`memberOf` on the user); `refint` keeps it consistent on
add/remove. **Admin permission checks read the group's `member` list**, not
`memberOf` on the user.
The SSO requires three groups (seeded automatically by the entrypoint /
`install.sh`):
| Group | Grants |
|-------|--------|
| `app_sso_admin` | full admin (users, groups, settings) |
| `app_sso_oauth_admin` | OAuth client management |
| `app_sso_invite` | invitation management |
## TLS (LDAPS / StartTLS)
The bundled slapd generates a **self-signed cert** on first start (CN =
`LDAP_CERT_CN`, valid 10y, SAN = CN + `localhost` + `127.0.0.1`) and listens on:
- `ldaps:///`**636**, TLS (the port to expose for direct-LDAP clients).
- `ldap:///`**389**, plain + StartTLS (not mapped to the host by default).
The cert lives on the `ldap-certs` volume so it persists across container
recreation.
### Trusting the self-signed cert
Copy it out and add it to the client's CA store:
```bash
docker compose cp sso-manager:/etc/openldap/certs/ldap.crt ./ldap.crt
```
…or, for quick LAN use, set `TLS_REQCERT never` on the client (the theta42/proxy
sets `app_ldap__tlsOptions__rejectUnauthorized=false` for the same effect).
### Using your own cert
Replace the `ldap-certs` named volume with a bind mount containing your own
`ldap.crt` + `ldap.key`:
```yaml
volumes:
- ./certs:/etc/openldap/certs # must contain ldap.crt + ldap.key
```
The entrypoint leaves existing certs untouched (idempotent).
## Direct-bind service accounts
For apps that bind LDAP directly, create a dedicated **service account** under
`ou=people` (e.g. `cn=ldapclient,ou=people,<base>`) with a strong password —
**don't reuse the admin DN**. The theta-env bootstrap creates this account
automatically (`cn=ldapclient`) and the proxy binds as it.
Example bind test:
```bash
ldapsearch -x -H ldaps://sso.example.com:636 \
-D "cn=ldapclient,ou=people,dc=yourdomain,dc=com" -W \
-b "ou=people,dc=yourdomain,dc=com" '(objectClass=posixAccount)' cn mail
```
## Modules + overlays (external LDAP servers)
If you point the app at your own LDAP server instead of the bundled slapd, it
needs:
- **Modules:** `pw-sha2` (the app stores user passwords as `{SSHA512}`),
`ppolicy`, `memberof`, `refint`.
- **Custom schema:** the `theta42Person` auxiliary objectClass with
`dateOfBirth` — see `ops/ldap-setup.sh` for the LDIF.
- **Directory tree:** `ou=people`, `ou=groups`, `ou=policies` under the base DN,
a default `pwdPolicy` at `cn=ppolicy,ou=policies,<base>`.
- **Required groups:** `app_sso_admin`, `app_sso_invite`, `app_sso_oauth_admin`.
`ops/ldap-setup.sh -p <admin-password>` configures all of the above
idempotently against a running slapd (auto-detects the database holding your
base DN, and verifies `pwdAccountLockedTime` is live — the attribute the app's
active/inactive toggle depends on).
## Backups
```bash
docker compose exec sso-manager slapcat -f /etc/openldap/slapd.conf \
-b "dc=yourdomain,dc=com" > ldap-backup-$(date +%F).ldif
```
Restorable with `ldapadd`/`ldapmodify` against a fresh instance.
## Troubleshooting
### `503 OpenLDAP ppolicy overlay is not configured`
The ppolicy overlay isn't attached to the database holding your users, so the
active/inactive toggle can't set `pwdAccountLockedTime`:
```bash
sudo ./ops/ldap-setup.sh -p 'admin-password' -b dc=yourdomain,dc=com
```
### LDAP connection refused
```bash
docker compose exec sso-manager sh -c 'ldapsearch -x -H ldap://localhost:389 -b "" -s base'
systemctl status slapd # bare metal
```
[← Back to Home](index.html)
+105
View File
@@ -0,0 +1,105 @@
---
layout: default
title: OAuth / OIDC
---
# OAuth 2.0 / OpenID Connect
[← Back to Home](index.html)
SSO Manager is an **OpenID Connect / OAuth 2.0 provider**: it issues its own
access, refresh, and ID tokens that your apps can consume to authenticate
users and authorize API calls. It also runs a full OpenLDAP directory, so it
can be both your SSO and your user directory at once.
## Discovery
The provider publishes a standards-compliant discovery document:
```
GET https://<sso-host>/.well-known/openid-configuration
```
It advertises the `issuer`, `authorization_endpoint`, `token_endpoint`,
`userinfo_endpoint`, `end_session_endpoint`, supported scopes, and token
lifetimes. OIDC clients (e.g. the theta42/proxy) can read their endpoint URLs
from here rather than configuring each one.
The `issuer` advertised is `conf.oauth.issuer` — set it to the **browser-facing**
HTTPS URL the SSO is served at (e.g. `https://sso.example.com`), either in
`conf/secrets.js` or via `app_oauth__issuer` / `OAUTH_ISSUER`.
## OAuth clients
An OAuth client represents an app that authenticates against the SSO. Each has:
- `client_id` (UUID) + `client_secret` (bcrypt-hashed; the **raw secret is
shown once** when the client is created or rotated — save it immediately).
- `name`, `description`, `created_by` (the admin uid that created it).
- `redirect_uris` — allowed callback URLs (must match exactly).
- `scopes` — requested scopes (default `openid profile email groups`).
- `allowed_groups` — restrict the client to members of specific SSO groups
(empty = any valid user).
- `token_lifetime``access_token` / `refresh_token` lifetimes (seconds).
### Managing clients
Clients are managed from the web UI (as a member of the `app_sso_oauth_admin`
group) or the HTTP API at `/api/oauth/client` (auth via the `auth-token` header
from a login):
| Method | Path | Action |
|--------|------|--------|
| `GET` | `/api/oauth/client` | list clients |
| `POST` | `/api/oauth/client` | create a client (returns the raw `client_secret` once) |
| `GET` | `/api/oauth/client/:id` | get one |
| `PUT` | `/api/oauth/client/:id` | update redirect URIs / scopes / groups |
| `DELETE` | `/api/oauth/client/:id` | delete |
| `POST` | `/api/oauth/client/:id/rotate` | rotate the secret (returns the new raw secret once) |
> All client-management endpoints are gated by the `app_sso_oauth_admin` group.
## Scopes
| Scope | Claims / access |
|-------|-----------------|
| `openid` | OIDC ID token + discovery |
| `profile` | `preferred_username`, display name, etc. |
| `email` | the user's `mail` |
| `groups` | the user's group memberships (the `groups` claim) |
The `groups` claim is what relying parties (e.g. the proxy's
`app_auth__adminGroups`) use to map group membership to roles.
## Token lifetimes
Defaults (overridable per-client via `token_lifetime`, or globally via
`app_oauth__token_lifetime__access_token` /
`app_oauth__token_lifetime__refresh_token`):
- access token: 3600s (1 hour)
- refresh token: 2592000s (30 days)
## Admin gating
SSO admin actions are gated by LDAP group membership (checked via the group's
`member` list, not `memberOf` on the user):
- `app_sso_admin` — full admin (users, groups, settings).
- `app_sso_oauth_admin` — OAuth client management.
- `app_sso_invite` — invitation management.
The bootstrap in [theta-env](https://github.com/theta42/theta-env) creates your
first admin and adds them to `app_sso_admin` + `app_sso_oauth_admin`
automatically; for a standalone install, add the admin's DN to those groups
manually (or via `ops/ldap-setup.sh`).
## JWT signing
Tokens are signed with `conf.oauth.jwtSecret` (`app_oauth__jwtSecret` /
`JWT_SECRET`). **Persist this secret** — if it changes, every issued token
stops validating. The all-in-one Docker image auto-generates one if none is set,
but that generated value does not survive container recreation unless you
persist it (set `JWT_SECRET` in your `.env`).
[← Back to Home](index.html)
Executable
+719
View File
@@ -0,0 +1,719 @@
#!/usr/bin/env bash
# install.sh - Idempotent standalone installer for Theta42 SSO Manager
# For Debian/Ubuntu systems
#
# This script:
# 1. Installs Node.js 20.x
# 2. Installs and configures OpenLDAP with required schemas/overlays
# 3. Deploys the SSO Manager application
# 4. Sets up systemd services
#
# Usage:
# sudo ./install.sh [OPTIONS]
#
# Options:
# -p, --admin-pass PASSWORD LDAP admin password (required, or set via LDAP_ADMIN_PASS env)
# -b, --base-dn DN Base DN (default: dc=example,dc=com)
# -n, --org-name NAME Organization name shown in UI/email (default: SSO Manager)
# -o, --port PORT HTTP port for SSO Manager (default: 3001)
# -j, --jwt-secret SECRET JWT secret for OAuth (default: auto-generated)
# -s, --smtp-config CONFIG SMTP config as host:port:user:pass
# --skip-ldap Skip LDAP installation (use existing LDAP)
# --skip-app Skip application installation (LDAP setup only)
# --dry-run Show what would be done without making changes
# -h, --help Show this help
#
# Environment variables (alternative to flags):
# LDAP_ADMIN_PASS, LDAP_BASE_DN, PORT, JWT_SECRET, SMTP_*
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────────────
BASE_DN="${LDAP_BASE_DN:-dc=example,dc=com}"
ADMIN_PASS="${LDAP_ADMIN_PASS:-}"
ORG_NAME="${ORG_NAME:-SSO Manager}"
PORT="${PORT:-3001}"
JWT_SECRET="${JWT_SECRET:-}"
SMTP_HOST="${SMTP_HOST:-}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_USER="${SMTP_USER:-}"
SMTP_PASS="${SMTP_PASS:-}"
SKIP_LDAP="${SKIP_LDAP:-false}"
SKIP_APP="${SKIP_APP:-false}"
DRY_RUN="${DRY_RUN:-false}"
INSTALL_DIR="/opt/sso-manager"
SYSTEMD_DIR="/etc/systemd/system"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# ── Helper functions ──────────────────────────────────────────────────────────
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
dry_run() { if [[ "$DRY_RUN" == "true" ]]; then echo "[DRY-RUN] $*"; fi; }
usage() {
grep '^#' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-p|--admin-pass)
ADMIN_PASS="$2"
shift 2
;;
-b|--base-dn)
BASE_DN="$2"
shift 2
;;
-n|--org-name)
ORG_NAME="$2"
shift 2
;;
-o|--port)
PORT="$2"
shift 2
;;
-j|--jwt-secret)
JWT_SECRET="$2"
shift 2
;;
-s|--smtp-config)
IFS=':' read -r SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASS <<< "$2"
shift 2
;;
--skip-ldap)
SKIP_LDAP="true"
shift
;;
--skip-app)
SKIP_APP="true"
shift
;;
--dry-run)
DRY_RUN="true"
shift
;;
-h|--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
# Validate required parameters
if [[ -z "$ADMIN_PASS" ]]; then
error "LDAP admin password is required (-p or LDAP_ADMIN_PASS env)"
exit 1
fi
# Generate JWT secret if not provided
if [[ -z "$JWT_SECRET" ]]; then
JWT_SECRET=$(openssl rand -hex 32)
info "Generated JWT secret: ${JWT_SECRET:0:8}..."
fi
# Derive the DNS domain from the base DN (dc=foo,dc=bar -> foo.bar) for email
# sender defaults. Override with LDAP_DOMAIN if set.
if [[ -z "${LDAP_DOMAIN:-}" ]]; then
LDAP_DOMAIN=$(echo "$BASE_DN" | sed 's/^dc=//; s/,dc=/./g')
fi
# ── System checks ─────────────────────────────────────────────────────────────
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (sudo)"
exit 1
fi
}
check_os() {
if [[ ! -f /etc/debian_version ]]; then
error "This script is for Debian/Ubuntu systems only"
exit 1
fi
info "Detected $(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
}
# ── Package installation ──────────────────────────────────────────────────────
install_package() {
local pkg="$1"
if dpkg -l | grep -q "^ii $pkg "; then
info "Package $pkg is already installed"
return 0
fi
dry_run "Would install package: $pkg"
[[ "$DRY_RUN" == "true" ]] && return 0
apt-get update -qq
apt-get install -y -qq "$pkg"
info "Installed $pkg"
}
install_nodejs() {
if command -v node &>/dev/null && node --version | grep -q "v20"; then
info "Node.js 20.x is already installed"
return 0
fi
dry_run "Would install Node.js 20.x"
[[ "$DRY_RUN" == "true" ]] && return 0
info "Installing Node.js 20.x..."
# Use NodeSource repository for Node.js 20.x
apt-get update -qq
apt-get install -y -qq curl gnupg ca-certificates
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
apt-get install -y -qq nodejs
info "Installed Node.js $(node --version)"
}
# ── OpenLDAP installation and configuration ───────────────────────────────────
install_openldap() {
if command -v slapd &>/dev/null; then
info "OpenLDAP is already installed"
return 0
fi
dry_run "Would install OpenLDAP"
[[ "$DRY_RUN" == "true" ]] && return 0
info "Installing OpenLDAP..."
# Pre-seed debconf for non-interactive installation
debconf-set-selections << EOF
slapd slapd/internal/adminpw string $ADMIN_PASS
slapd slapd/password1 string $ADMIN_PASS
slapd slapd/password2 string $ADMIN_PASS
slapd slapd/domain string ${BASE_DN#dc=}
slapd slapd/backend string MDB
slapd shared/organization string $ORG_NAME
slapd slapd/purge_database boolean true
slapd slapd/move_old_database boolean true
slapd slapd/invalid_config boolean true
EOF
apt-get update -qq
apt-get install -y -qq slapd ldap-utils
# Configure ldap.conf
cat > /etc/ldap/ldap.conf << LDAPCONF
BASE $BASE_DN
URI ldap://localhost
LDAPCONF
# Set proper permissions
chmod 644 /etc/ldap/ldap.conf
info "OpenLDAP installed"
}
configure_openldap() {
info "Configuring OpenLDAP..."
dry_run "Would configure OpenLDAP with base DN: $BASE_DN"
[[ "$DRY_RUN" == "true" ]] && return 0
# Wait for slapd to be ready
for i in {1..10}; do
if ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=*)" dn >/dev/null 2>&1; then
info "OpenLDAP is ready"
break
fi
sleep 1
done
# Detect the database DN for our suffix
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
"(&(objectClass=olcDatabaseConfig)(olcSuffix=${BASE_DN}))" dn 2>/dev/null \
| grep "^dn:" | head -1 | sed 's/^dn: //')
if [[ -z "$DB_DN" ]]; then
# Try to find any database and update its suffix
DB_DN=$(ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" \
"(objectClass=olcDatabaseConfig)" dn 2>/dev/null \
| grep "^dn:" | head -1 | sed 's/^dn: //')
if [[ -n "$DB_DN" ]]; then
info "Updating database suffix to $BASE_DN"
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: $DB_DN
changetype: modify
replace: olcSuffix
olcSuffix: $BASE_DN
EOF
fi
fi
if [[ -z "$DB_DN" ]]; then
error "Could not detect OpenLDAP database configuration"
return 1
fi
info "Using database: $DB_DN"
# 1. Load pw-sha2 module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "pw-sha2"; then
info "Loading pw-sha2 module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: pw-sha2
EOF
else
info "pw-sha2 module already loaded"
fi
# 2. Load ppolicy module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "ppolicy"; then
info "Loading ppolicy module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy
EOF
else
info "ppolicy module already loaded"
fi
# 3. Load memberof module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "memberof"; then
info "Loading memberof module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{1},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof
EOF
else
info "memberof module already loaded"
fi
# 4. Load refint module
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=config" "(objectClass=olcModuleList)" olcModuleLoad 2>/dev/null | grep -q "refint"; then
info "Loading refint module..."
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=module{1},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: refint
EOF
else
info "refint module already loaded"
fi
# 5. Add ppolicy overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*ppolicy*)" dn 2>/dev/null | grep -qi "ppolicy"; then
info "Adding ppolicy overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=ppolicy,$DB_DN
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=ppolicy,ou=policies,$BASE_DN
olcPPolicyUseLockout: TRUE
EOF
else
info "ppolicy overlay already configured"
fi
# 6. Add memberof overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*memberof*)" dn 2>/dev/null | grep -qi "memberof"; then
info "Adding memberof overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=memberof,$DB_DN
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
EOF
else
info "memberof overlay already configured"
fi
# 7. Add refint overlay
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "$DB_DN" "(olcOverlay=*refint*)" dn 2>/dev/null | grep -qi "refint"; then
info "Adding refint overlay..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: olcOverlay=refint,$DB_DN
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
objectClass: top
olcOverlay: refint
olcRefintAttribute: memberof member manager owner
EOF
else
info "refint overlay already configured"
fi
# 8. Add database indexes
info "Configuring database indexes..."
for index in "mail eq,sub" "uid eq,sub" "cn eq,sub" "member eq" "uidNumber eq" "gidNumber eq"; do
attr=$(echo "$index" | cut -d' ' -f1)
types=$(echo "$index" | cut -d' ' -f2)
ldapmodify -Q -Y EXTERNAL -H ldapi:/// << EOF || true
dn: $DB_DN
changetype: modify
add: olcDbIndex
olcDbIndex: $attr $types
EOF
done
# 9. Load custom theta42 schema
if ! ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b "cn=schema,cn=config" "(olcObjectClasses=*theta42Person*)" olcObjectClasses 2>/dev/null | grep -q "theta42"; then
info "Loading custom theta42 schema..."
ldapadd -Q -Y EXTERNAL -H ldapi:/// << EOF
dn: cn=theta42,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: theta42
olcAttributeTypes: ( 1.3.6.1.4.1.99999.1.1
NAME 'dateOfBirth'
DESC 'Date of birth in ISO 8601 format YYYY-MM-DD'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcObjectClasses: ( 1.3.6.1.4.1.99999.2.1
NAME 'theta42Person'
DESC 'Theta42 SSO extended person attributes'
AUXILIARY
MAY ( dateOfBirth ) )
EOF
else
info "theta42 schema already loaded"
fi
# 10. Create base directory structure
BIND_DN="cn=admin,$BASE_DN"
# Create base DN if it doesn't exist
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating base DN structure..."
DC_VALUE="${BASE_DN#dc=}"
DC_VALUE="${DC_VALUE%%,*}"
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: $BASE_DN
objectClass: dcObject
objectClass: organization
dc: $DC_VALUE
o: $ORG_NAME
EOF
else
info "Base DN already exists"
fi
# Create OUs
for ou in people groups policies; do
dn="ou=$ou,$BASE_DN"
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating $ou OU..."
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: ou=$ou,$BASE_DN
objectClass: organizationalUnit
ou: $ou
EOF
else
info "OU $ou already exists"
fi
done
# 11. Create default ppolicy
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "cn=ppolicy,ou=policies,$BASE_DN" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating default ppolicy..."
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: cn=ppolicy,ou=policies,$BASE_DN
objectClass: top
objectClass: organizationalRole
objectClass: pwdPolicy
cn: ppolicy
pwdAttribute: 2.5.4.35
pwdLockout: FALSE
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
EOF
else
info "Default ppolicy already exists"
fi
# 12. Create required SSO groups
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
dn="cn=$group,ou=groups,$BASE_DN"
if ! ldapsearch -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// -b "$dn" -s base "(objectClass=*)" dn 2>/dev/null | grep -q "dn:"; then
info "Creating group: $group"
ldapadd -x -D "$BIND_DN" -w "$ADMIN_PASS" -H ldapi:/// << EOF
dn: $dn
objectClass: groupOfNames
objectClass: top
cn: $group
description: $ORG_NAME $group group
member: $BIND_DN
EOF
else
info "Group $group already exists"
fi
done
info "OpenLDAP configuration complete"
}
# ── Application installation ──────────────────────────────────────────────────
install_app() {
info "Installing SSO Manager application..."
dry_run "Would install application to $INSTALL_DIR"
[[ "$DRY_RUN" == "true" ]] && return 0
# Create installation directory
mkdir -p "$INSTALL_DIR"
# Copy application files
info "Copying application files..."
cp -r "$SCRIPT_DIR/nodejs/"* "$INSTALL_DIR/"
# Install npm dependencies
info "Installing npm dependencies..."
cd "$INSTALL_DIR"
npm ci --only=production --quiet
# Create secrets configuration
info "Creating application configuration..."
cat > "$INSTALL_DIR/conf/secrets.js" << SECRETEOF
'use strict';
module.exports = {
port: $PORT,
ldap: {
url: 'ldap://localhost',
bindDN: 'cn=admin,$BASE_DN',
bindPassword: '$ADMIN_PASS',
userBase: 'ou=people,$BASE_DN',
groupBase: 'ou=groups,$BASE_DN',
},
smtp: {
host: '${SMTP_HOST:-localhost}',
port: ${SMTP_PORT:-587},
user: '${SMTP_USER:-}',
pass: '${SMTP_PASS:-}',
from: '${ORG_NAME} <noreply@${LDAP_DOMAIN}>',
},
voipms: {
username: '${VOIPMS_USER:-}',
password: '${VOIPMS_PASS:-}',
did: '${VOIPMS_DID:-}',
},
oauth: {
issuer: '',
jwtSecret: '$JWT_SECRET',
token_lifetime: {
access_token: 3600,
refresh_token: 2592000
}
},
};
SECRETEOF
# Create base configuration
cat > "$INSTALL_DIR/conf/base.js" << BASEEOF
'use strict';
module.exports = {
name: "$ORG_NAME",
userModel: 'ldap',
redis: {
prefix: 'sso_manager_'
},
ldap: {
url: 'ldap://localhost',
bindDN: 'cn=admin,$BASE_DN',
bindPassword: '__IN SECRETS FILE__',
userBase: 'ou=people,$BASE_DN',
groupBase: 'ou=groups,$BASE_DN',
userFilter: '(objectClass=posixAccount)',
userNameAttribute: 'uid'
},
oauth: {
issuer: '',
jwtSecret: '__in secrets file__',
token_lifetime: {
access_token: 3600,
refresh_token: 2592000
}
},
smtp: {
host: 'localhost',
port: 587,
secure: false,
from: '$ORG_NAME <noreply@$LDAP_DOMAIN>',
},
};
BASEEOF
# Set ownership
chown -R root:root "$INSTALL_DIR"
chmod -R 755 "$INSTALL_DIR"
info "Application installed to $INSTALL_DIR"
}
# ── Systemd service configuration ─────────────────────────────────────────────
install_systemd() {
info "Installing systemd service..."
dry_run "Would install systemd service"
[[ "$DRY_RUN" == "true" ]] && return 0
cat > "$SYSTEMD_DIR/sso-manager.service" << UNITEOF
[Unit]
Description=Theta42 SSO Manager
Documentation=file://$INSTALL_DIR/README.md
After=network.target slapd.service
Wants=slapd.service
[Service]
Type=simple
User=root
WorkingDirectory=$INSTALL_DIR
ExecStart=/usr/bin/node $INSTALL_DIR/bin/www
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=NODE_PORT=$PORT
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
UNITEOF
systemctl daemon-reload
systemctl enable sso-manager.service
info "Systemd service installed"
}
# ── Verification ──────────────────────────────────────────────────────────────
verify_installation() {
info "Verifying installation..."
local errors=0
# Check OpenLDAP
if command -v slapd &>/dev/null; then
if systemctl is-active --quiet slapd; then
info "✓ OpenLDAP is running"
else
warn "✗ OpenLDAP is not running"
((errors++))
fi
else
warn "✗ OpenLDAP is not installed"
((errors++))
fi
# Check application
if [[ -d "$INSTALL_DIR" ]]; then
info "✓ Application is installed"
else
warn "✗ Application is not installed"
((errors++))
fi
# Check systemd service
if systemctl is-enabled --quiet sso-manager.service 2>/dev/null; then
info "✓ Systemd service is enabled"
else
warn "✗ Systemd service is not enabled"
((errors++))
fi
if [[ $errors -eq 0 ]]; then
info "Installation verified successfully"
else
warn "Installation completed with $errors issue(s)"
fi
return $errors
}
# ── Main execution ────────────────────────────────────────────────────────────
main() {
echo
echo "=============================================="
echo " Theta42 SSO Manager Installer"
echo "=============================================="
echo
echo "Configuration:"
echo " Base DN: $BASE_DN"
echo " Port: $PORT"
echo " Install dir: $INSTALL_DIR"
echo " Skip LDAP: $SKIP_LDAP"
echo " Skip App: $SKIP_APP"
echo
check_root
check_os
if [[ "$SKIP_LDAP" != "true" ]]; then
echo
info "=== Installing OpenLDAP ==="
install_openldap
configure_openldap
fi
if [[ "$SKIP_APP" != "true" ]]; then
echo
info "=== Installing SSO Manager ==="
install_nodejs
install_app
install_systemd
fi
echo
verify_installation
echo
echo "=============================================="
echo " Installation Complete!"
echo "=============================================="
echo
if [[ "$SKIP_APP" != "true" ]]; then
info "Start the service with: systemctl start sso-manager"
info "View logs with: journalctl -fu sso-manager"
info "Access the UI at: http://localhost:$PORT"
fi
if [[ "$SKIP_LDAP" != "true" ]]; then
echo
info "LDAP Configuration:"
info " Base DN: $BASE_DN"
info " Bind DN: cn=admin,$BASE_DN"
info " Admin pass: (set by you)"
echo
info "Required SSO groups created:"
info " - app_sso_admin"
info " - app_sso_invite"
info " - app_sso_oauth_admin"
fi
echo
}
main
+18 -11
View File
@@ -1,22 +1,29 @@
'use strict';
// Base configuration — generic defaults usable by anyone.
//
// These are NON-secret defaults. Per-deployment values (LDAP bind DN, user/group
// bases, SMTP host/user, OAuth issuer, sender address) should be overridden via
// conf/secrets.js or `app_*` environment variables (see @simpleworkjs/conf).
// Secret values (passwords, JWT secret, API keys) MUST come from secrets.js or
// `app_*` env vars — never commit them here.
module.exports = {
name: "Theta42 SSO",
name: "SSO Manager", // displayed in the UI and outbound email
userModel: 'ldap', // pam, redis, ldap
redis: {
prefix: 'sso_manager_'
},
ldap: {
url: 'ldaps://ldap.internal.theta42.com:636',
bindDN: 'cn=admin,dc=theta42,dc=com',
bindPassword: '__IN SRECREST FILE__',
userBase: 'ou=people,dc=theta42,dc=com',
groupBase: 'ou=groups,dc=theta42,dc=com',
url: 'ldap://localhost',
bindDN: 'cn=admin,dc=example,dc=com',
bindPassword: '__in secrets file__',
userBase: 'ou=people,dc=example,dc=com',
groupBase: 'ou=groups,dc=example,dc=com',
userFilter: '(objectClass=posixAccount)',
userNameAttribute: 'uid'
},
oauth: {
issuer: 'https://sso.theta42.com',
issuer: '', // falls back to the request host at runtime (routes/index.js)
jwtSecret: '__in secrets file__',
token_lifetime: {
access_token: 3600, // 1 hour (seconds)
@@ -29,11 +36,11 @@ module.exports = {
did: '__in secrets file__',
},
smtp: {
host: 'mail.wgnode.com',
host: 'localhost',
port: 587,
secure: false,
user: 'noreply@users.theta42.com',
user: 'noreply@example.com',
pass: '__in secrets file__',
from: 'Theta42 Accounts <noreply@users.theta42.com>',
from: 'SSO Manager <noreply@example.com>',
},
};
};
+4 -4
View File
@@ -11,7 +11,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8",
"@simpleworkjs/conf": "^1.0.0",
"@simpleworkjs/conf": "^1.1.0",
"bcrypt": "^6.0.0",
"bootstrap": "^5.3.8",
"ejs": "^3.1.10",
@@ -1126,9 +1126,9 @@
}
},
"node_modules/@simpleworkjs/conf": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.0.0.tgz",
"integrity": "sha512-p1dQAELW0oUBRpDoz260TYw18IMI/Y11xYAb17P1MEPjsTAUB0LWE/6ZeA2VQmpU/LXoRnDysg0G/oASGILyUA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@simpleworkjs/conf/-/conf-1.1.0.tgz",
"integrity": "sha512-MKRQQ4JAH2tbEm87NdkmfikTT58Tyk/SFbvCC7zKja0bK6j8zYyBXTQUJ0rnvFOVEalDWd/au4AEiptOCEqgvA==",
"license": "MIT",
"dependencies": {
"extend": "^3.0.2"
+1 -1
View File
@@ -23,7 +23,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^7.3.0",
"@popperjs/core": "^2.11.8",
"@simpleworkjs/conf": "^1.0.0",
"@simpleworkjs/conf": "^1.1.0",
"bcrypt": "^6.0.0",
"bootstrap": "^5.3.8",
"ejs": "^3.1.10",
+5
View File
@@ -39,6 +39,11 @@ frontEndModules.forEach(dep => {
// local folder.
router.use('/static', express.static(path.join(__dirname, '../public')))
// Public health endpoint for container/orchestration healthchecks.
// Mounted at / (no auth) in app.js, so this is intentionally unauthenticated.
router.get('/health', function(req, res) {
res.json({ status: 'ok' });
});
router.get('/tos', function(req, res) {
res.render('tos', {...values, tosHtml});
+3
View File
@@ -0,0 +1,3 @@
dn: olcDatabase={1}mdb,cn=config
add: olcDbIndex
olcDbIndex: mail eq,sub
+7
View File
@@ -0,0 +1,7 @@
dn: ou=people,dc=theta42,dc=com
objectClass: organizationalUnit
ou: People
dn: ou=groups,dc=theta42,dc=com
objectClass: organizationalUnit
ou: Groups
+4
View File
@@ -0,0 +1,4 @@
dn: cn=config
changetype: modify
replace: olcLogLevel
olcLogLevel: stats
+4
View File
@@ -0,0 +1,4 @@
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof.la
+17
View File
@@ -0,0 +1,17 @@
dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModuleLoad: memberof
olcModulePath: /usr/lib/ldap
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
+3
View File
@@ -0,0 +1,3 @@
dn: cn=module{1},cn=config
add: olcmoduleload
olcmoduleload: refint
+7
View File
@@ -0,0 +1,7 @@
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
objectClass: top
olcOverlay: {1}refint
olcRefintAttribute: memberof member manager owner
+26
View File
@@ -0,0 +1,26 @@
# Entry 1: cn=stuff-manager,ou=groups,dc=theta42,dc=com
dn: cn=stuff-manager,ou=groups,dc=theta42,dc=com
cn: stuff-manager
gidnumber: 1498
objectclass: posixGroup
objectclass: top
# Entry 1: cn=stuff-manager,ou=people,dc=theta42,dc=com
dn: cn=stuff-manager,ou=people,dc=theta42,dc=com
cn: stuff-manager
gidnumber: 1498
givenname: Stuff
homedirectory: /home/stuff-manager
loginshell: /bin/bash
objectclass: inetOrgPerson
objectclass: sudoRole
objectclass: ldapPublicKey
objectclass: posixAccount
objectclass: top
sn: Manager
sudocommand: ALL
sudohost: ALL
sudouser: stuff-manager
uid: stuff-manager
uidnumber: 1498
userpassword: {MD5}YSv5lro/uQ0B7i81kGDPVQ==
+7
View File
@@ -0,0 +1,7 @@
dn: cn=config
changetype: modify
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/sasl2/ldap-server.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/sasl2/ldap-server.key
+42
View File
@@ -0,0 +1,42 @@
'use strict';
// Example secrets configuration file (file-based config, for non-Docker use).
// Copy to nodejs/conf/secrets.js and fill in your values.
//
// Docker users: you usually don't need this file — pass `app_*` env vars instead
// (see DEPLOYMENT.md). This file is for bare-metal / mounted-config deployments.
// Values here override conf/base.js and win over <environment>.js. `app_*` env
// vars (if any are set) override this file too.
module.exports = {
port: 3001,
name: 'SSO Manager', // shown in UI and outbound email
ldap: {
url: 'ldap://localhost', // or ldaps://host:636 for TLS
bindDN: 'cn=admin,dc=example,dc=com',
bindPassword: 'your-ldap-password',
userBase: 'ou=people,dc=example,dc=com',
groupBase: 'ou=groups,dc=example,dc=com',
},
smtp: {
host: 'smtp.example.com',
port: 587,
secure: false, // true for 465, false for other ports
user: 'noreply@example.com',
pass: 'your-smtp-password',
from: 'SSO Manager <noreply@example.com>',
},
voipms: {
username: '', // VoIP.ms username (optional)
password: '', // VoIP.ms password (optional)
did: '', // VoIP.ms DID (optional)
},
oauth: {
issuer: '', // falls back to the request host at runtime
jwtSecret: 'generate-a-secure-random-string-here',
token_lifetime: {
access_token: 3600, // 1 hour in seconds
refresh_token: 2592000 // 30 days in seconds
}
},
};