Add self-service API tokens (PATs) with UI + Bearer auth (#35)

Personal access tokens so scripts/CI can call the management API without a
browser session. Each logged-in user mints their own token; it authenticates as
the creator (carries their LDAP group permissions, re-resolved live), so the
existing permission.byGroup checks apply unchanged.

- models/api_token.js: new ApiToken model (sso_<id>_<secret> format; id is the
  lookup key, secret bcrypt-hashed + isPrivate, shown once). add()/rotate()/
  authenticate(); optional expires_at; best-effort last_used_on. No _ttl
  (persists; lifetime via expires_at).
- routes/api_token.js: self-service CRUD (list/get/update/delete/rotate),
  owner-scoped (created_by === req.user.uid, 403 otherwise).
- middleware/auth.js + models/auth.js: accept `Authorization: Bearer sso_...`
  (precedence over the auth-token session header); checkApiToken collapses
  every failure to one generic 401 (no existence/secret/expiry leak).
- views/api_tokens.ejs + routes/index.js (GET /api-tokens): self-service page
  (forceLogin, no group gate) — create (token shown once), edit, rotate, revoke.
- views/top.ejs: "API Tokens" nav entry visible to all logged-in users.
- public/js/app.js: app.apiToken client module.
- DEPLOYMENT.md + docs/deployment.md: API tokens section.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:12:35 -04:00
committed by GitHub
parent 6920a9f9f0
commit b91ef2792d
12 changed files with 546 additions and 0 deletions
+25
View File
@@ -108,6 +108,31 @@ bare-metal / advanced standalone use; most deployments should use the file.
- LDAP (internal, app↔slapd): `ldap://localhost:389` (not mapped to the host)
- LDAPS (for legacy apps / direct binds): `ldaps://<host>:636` (TLS)
### API tokens (personal access tokens)
Any logged-in user can mint a long-lived bearer token to call the management
API from scripts/CI/other services, without a browser session. Tokens are
self-service and authenticate **as their creator** — a token carries the
creator's LDAP group permissions, so the same `permission.byGroup` checks apply
(group membership is re-resolved from LDAP live on each request).
Create one in the UI under **API Tokens** (the token string is shown **once**),
then use it as a bearer token:
```bash
curl -H "Authorization: Bearer sso_<id>_<secret>" https://sso.example.com/api/user
```
Format: `sso_<id>_<secret>` — the `id` is the lookup key, the `secret` is
bcrypt-hashed and never stored in plaintext. Rotate or revoke a token from the
same UI page; revocation takes effect immediately. Optional expiry (in days) at
creation. API tokens persist in the bundled Redis, so they survive rebuilds
(Redis is persisted via AOF — see *Backups and restore*).
The token has the same access as a browser session for that user — an
`app_sso_admin`'s token can manage users/groups; a non-admin's token is limited
to what they could do in the UI.
### Logs
The all-in-one image runs the Node app and slapd (OpenLDAP) in one container,