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:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user