From 158109de596ea58740ffbb2982c1fdc18e206d5a Mon Sep 17 00:00:00 2001 From: William Mantly Date: Mon, 13 Jul 2026 23:21:27 -0400 Subject: [PATCH] Documentation cleanup for public release (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: cleanup for public release (fix stale/wrong API docs, LICENSE, versions) Documentation cleanup ahead of the public release announcement. Fixes a set of confirmed issues from a prior audit: - LICENSE: fill in MIT template placeholders (theta42, 2026). - README.md: fix broken API docs link (api.md -> API.md), correct required Node.js version (13.x -> 20.x), add the missing app_sso_invite group to the LDAP groups table, scrub hardcoded dc=theta42,dc=com to the generic dc=example,dc=com used elsewhere, add a "Recommended: Docker or install.sh" section pointing to DEPLOYMENT.md/docs before the manual OpenLDAP walkthrough, and drop an emoji from a warning callout. - nodejs/api.md: deleted — it was a stale/legacy doc with wrong routes, wrong request bodies, and endpoints that are dead/commented-out code. The root API.md is the accurate, current reference; README now links there directly. - API.md: add the missing app_sso_invite permission group, fix the documented invite response to match the real {token, link, mail_sent} payload, document the previously-undocumented GET/PUT/DELETE /api/user/invite endpoints, add the real allowed_groups field to the OAuth client management examples, and document POST /api/oauth/authorize (the endpoint that actually issues the code after consent). - nodejs/routes/auth.js + API.md: fix "emaill address" typo in the password-reset response message (source and docs kept in sync). - DEPLOYMENT.md: fix the top-level summary to mention Redis, matching docs/deployment.md and the entrypoint behavior it already documents. Flagged, not changed: tos.md reads like a personal home-lab acceptable-use policy (Emby/Gitea/Proxmox/Discord/Signal, first-person "the admin") rather than generic OSS docs. Left in place pending a manual decision to genericize, relocate, or remove it. Co-Authored-By: Claude Sonnet 5 * docs: genericize tos.md template, track runtime-editable terms in #39 Removes operator-specific references (Emby, Gitea, Proxmox, Discord, Signal, first-person "the admin") so the shipped tos.md reads as a neutral starting template rather than one operator's internal policy. Actual runtime editability (admin/legal editing terms without a code change) is tracked in issue #39, not implemented here. --------- Co-authored-by: Claude Sonnet 5 --- API.md | 130 ++++++++++++++++++++++++++++++++++++++++-- DEPLOYMENT.md | 6 +- LICENSE | 2 +- README.md | 40 +++++++++---- nodejs/api.md | 60 ------------------- nodejs/routes/auth.js | 2 +- tos.md | 32 +++++++---- 7 files changed, 180 insertions(+), 92 deletions(-) delete mode 100755 nodejs/api.md diff --git a/API.md b/API.md index 981f9d6..6814c41 100644 --- a/API.md +++ b/API.md @@ -38,6 +38,7 @@ auth-token: |-------|--------| | `app_sso_admin` | Full user/group/notification management | | `app_sso_oauth_admin` | Register and manage OAuth clients | +| `app_sso_invite` | Invitation management | | Group owner | Manage membership of that specific group | Self-service: users can always read and modify their own account without admin membership. @@ -116,7 +117,7 @@ Returns available username suggestions based on name and optional date of birth. **Response:** ```json -{ "message": "If the emaill address is in our system, you will receive a message." } +{ "message": "If the email address is in our system, you will receive a message." } ``` The same response is returned whether or not the email exists. Reset tokens expire after 24 hours. @@ -506,11 +507,90 @@ When an admin changes another user's password, `password_must_change` is set on ### Generate Invite Token -**`POST /api/user/invite`** — Any authenticated user +Create an invite token, optionally emailing it to the invitee and pre-assigning +LDAP groups the new account should be added to on signup. + +**`POST /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required + +**Request:** +```json +{ "mail": "invitee@example.com", "groups": ["group_name"] } +``` + +`mail` and `groups` are both optional. If `mail` is provided, a verification +email is sent to that address. **Response:** ```json -{ "token": "invite_token_string" } +{ + "token": "invite_token_string", + "link": "https://your-domain.com/login/invite/invite_token_string", + "mail_sent": true +} +``` + +--- + +### List Invite Tokens + +**`GET /api/user/invite`** — `app_sso_admin` or `app_sso_invite` required + +Admins (`app_sso_admin`) see all invite tokens; non-admin `app_sso_invite` +members see only invites they created. + +**Response:** +```json +{ + "results": [ + { + "token": "invite_token_string", + "created_by": "username", + "created_on": 1234567890000, + "is_valid": true, + "mail": "invitee@example.com", + "groups": "[\"group_name\"]" + } + ] +} +``` + +--- + +### Update Invite Token + +Update the groups an invite will assign, or change/clear the invitee's email +(re-sends verification if a new email is set). + +**`PUT /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite` +member who created the invite + +**URL Parameters:** `token` — invite token + +**Request:** Any subset of: +```json +{ "groups": ["group_name"], "mail": "newinvitee@example.com" } +``` + +Set `mail` to `null`/empty to clear it. Fails with `400` if the token is no +longer valid. + +**Response:** +```json +{ "results": { "token": "invite_token_string", "is_valid": true, "...": "..." } } +``` + +--- + +### Revoke Invite Token + +**`DELETE /api/user/invite/:token`** — `app_sso_admin`, or the `app_sso_invite` +member who created the invite + +**URL Parameters:** `token` — invite token + +**Response:** +```json +{ "results": true } ``` --- @@ -831,7 +911,44 @@ Returns the OIDC discovery document with endpoint URLs, supported scopes, and si - `code_challenge` — PKCE challenge (SHA-256 of code_verifier, base64url-encoded) - `code_challenge_method` — Must be `S256` -Renders the consent screen. On approval, redirects to `redirect_uri?code=&state=`. +Renders the consent screen. + +--- + +### Authorize (Issue Code) + +Issues the authorization code after the user approves the consent form shown +by the Authorization Endpoint above. This is called by the consent page itself +(an authenticated request, via `auth-token`), not by the OAuth client +directly. + +**`POST /api/oauth/authorize`** — Auth required (`auth-token` header) + +**Request:** +```json +{ + "response_type": "code", + "client_id": "uuid", + "redirect_uri": "https://ha.example.com/auth/external/callback", + "scope": "openid profile email", + "state": "opaque-state-value", + "code_challenge": "pkce-challenge", + "code_challenge_method": "S256" +} +``` + +Only scopes the client is actually registered for are granted, even if more +are requested. If the client has `allowed_groups` set, the authenticated user +must be a member of at least one of those groups or the request is rejected +with `403`. + +**Response:** +```json +{ "redirect_url": "https://ha.example.com/auth/external/callback?code=&state=" } +``` + +The caller (the consent page) redirects the browser to `redirect_url`, which +completes the flow described in the Authorization Endpoint section above. --- @@ -916,6 +1033,7 @@ All endpoints require authentication and `app_sso_oauth_admin` membership. "description": "Home automation", "redirect_uris": ["https://ha.example.com/auth/external/callback"], "scopes": ["openid", "profile", "email"], + "allowed_groups": [], "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 }, "created_by": "wmantly", "created_on": 1234567890000 @@ -937,11 +1055,13 @@ All endpoints require authentication and `app_sso_oauth_admin` membership. "description": "Home automation dashboard", "redirect_uris": ["https://ha.example.com/auth/external/callback"], "scopes": ["openid", "profile", "email"], + "allowed_groups": [], "token_lifetime": { "access_token": 3600, "refresh_token": 2592000 } } ``` `redirect_uris` may also be a newline-separated string. `scopes` may also be a space-separated string. +`allowed_groups` restricts the client to members of the listed SSO groups (empty/omitted = any valid user). **Response:** ```json @@ -968,7 +1088,7 @@ The `client_secret` is shown **only once**. Store it immediately. **`PUT /api/oauth/client/:client_id`** -**Request:** Any subset of `name`, `description`, `redirect_uris`, `scopes`, `token_lifetime`, `is_valid`. +**Request:** Any subset of `name`, `description`, `redirect_uris`, `scopes`, `allowed_groups`, `token_lifetime`, `is_valid`. **Response:** `{ "results": { }, "message": "..." }` diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 47ce35a..53e1702 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -2,8 +2,8 @@ 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). +1. **Docker** — a single all-in-one image bundling the app + OpenLDAP + Redis (`docker compose up`). +2. **Bare metal** — `install.sh` on Debian/Ubuntu (installs Node.js, OpenLDAP, Redis, the app, and a systemd unit). ## How configuration works @@ -39,7 +39,7 @@ as raw strings otherwise. Examples: ## Method 1: Docker (all-in-one) -The image (`Dockerfile.openldap`) bundles OpenLDAP and the app in one container. +The image (`Dockerfile.openldap`) bundles OpenLDAP, Redis, 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. diff --git a/LICENSE b/LICENSE index 3f28a5f..134193a 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) +Copyright (c) 2026 theta42 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 51a1915..9ce91cb 100755 --- a/README.md +++ b/README.md @@ -1,12 +1,31 @@ # SSO manager ## API docs -[API docs](api.md) +[API docs](API.md) ## Server set up +### Recommended: Docker or install.sh + +The supported, tested deployment paths are **Docker** (an all-in-one image +bundling the app + OpenLDAP + Redis) and the **`install.sh`** bare-metal +installer for Debian/Ubuntu. Most users should start there: + +- [Deployment Guide](DEPLOYMENT.md) — Docker + bare metal, configuration + layers, backups, troubleshooting. +- [docs/index.md](docs/index.md) — documentation home (also published as a + GitHub Pages site), with links to configuration, LDAP, and OAuth/OIDC docs. + +### Manual / advanced: raw OpenLDAP setup + +The rest of this section walks through configuring OpenLDAP by hand. This is +the **manual/advanced path** — useful if you're pointing the app at an +existing LDAP server, or want to understand exactly what `install.sh` and the +Docker entrypoint automate for you. Most deployments should use Docker or +`install.sh` above instead. + The server requires: -* NodeJS 13.x +* NodeJS 20.x * LDAP server > Setting up the whole stack (Docker) or want the secrets-file layout? See @@ -63,11 +82,11 @@ EOF **2. Add the overlay to your user database:** -> ⚠️ The database index below (`{1}mdb`) is **not** the same on every install. Confirm yours first — the overlay must go on the database whose `olcSuffix` is your base DN, or account locking silently won't apply to your users: +> Warning: The database index below (`{1}mdb`) is **not** the same on every install. Confirm yours first — the overlay must go on the database whose `olcSuffix` is your base DN, or account locking silently won't apply to your users: > > ```bash > ldapsearch -Q -Y EXTERNAL -H ldapi:/// -b cn=config \ -> '(&(objectClass=olcDatabaseConfig)(olcSuffix=dc=theta42,dc=com))' dn +> '(&(objectClass=olcDatabaseConfig)(olcSuffix=dc=example,dc=com))' dn > ``` ```bash @@ -76,7 +95,7 @@ dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig olcOverlay: ppolicy -olcPPolicyDefault: cn=ppolicy,ou=policies,dc=theta42,dc=com +olcPPolicyDefault: cn=ppolicy,ou=policies,dc=example,dc=com olcPPolicyUseLockout: TRUE olcPPolicyHashCleartext: FALSE EOF @@ -85,12 +104,12 @@ EOF **3. Create the policies container and default policy:** ```bash -ldapadd -x -D "cn=admin,dc=theta42,dc=com" -W << 'EOF' -dn: ou=policies,dc=theta42,dc=com +ldapadd -x -D "cn=admin,dc=example,dc=com" -W << 'EOF' +dn: ou=policies,dc=example,dc=com objectClass: organizationalUnit ou: policies -dn: cn=ppolicy,ou=policies,dc=theta42,dc=com +dn: cn=ppolicy,ou=policies,dc=example,dc=com objectClass: top objectClass: organizationalRole objectClass: pwdPolicy @@ -105,8 +124,8 @@ EOF Verify by locking a test account and confirming bind fails: ```bash -ldapmodify -x -D "cn=admin,dc=theta42,dc=com" -W << 'EOF' -dn: cn=testuser,ou=people,dc=theta42,dc=com +ldapmodify -x -D "cn=admin,dc=example,dc=com" -W << 'EOF' +dn: cn=testuser,ou=people,dc=example,dc=com changetype: modify replace: pwdAccountLockedTime pwdAccountLockedTime: 000001010000Z @@ -153,6 +172,7 @@ ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=theta42,cn=schema,cn=config" olcAttri |-------|---------| | `app_sso_admin` | Full admin access: manage users, groups, OAuth clients | | `app_sso_oauth_admin` | Manage OAuth clients only | +| `app_sso_invite` | Invitation management | ## Logs (Docker) diff --git a/nodejs/api.md b/nodejs/api.md deleted file mode 100755 index 70bc238..0000000 --- a/nodejs/api.md +++ /dev/null @@ -1,60 +0,0 @@ -## create invite token - -**post** `/users/invite` - -```bash -curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f-b8df55431cd1" -X POST https://proxy-host.com/users/invite -``` - -* 200 {"token":"5caf94d2-2c91-4010-8df7-968d10802b9d"} - - -## sing up - -**post** `/auth/invite/` - -```bash -curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \"password\": \"palm7\"}" https://proxy-host.com/auth/invite/b33d8819-ec64-4cf4-a6ec-77562d738fa4 - -``` - -* 200 {"user":"test9","token":"af662d8b-3d44-4110-8ad9-047dc752d97f"} -* 400 {"message":"Missing fields"} -* 401 {"message":"Token not valid"} -* 409 {"message":"username taken"} - - -## login - -**post** `/auth/login` - -```bash -curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "password": "mypassword"}' https://proxy-host.com/auth/login -``` - -* 200 {"login":true,"token":"027d3964-7d81-4462-a6f9-2c1f9b40b4be"} -* 401 {"login":false} - - -## verify SSH key - -**post** `/auth/verifykey` - -```bash -curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9vboz5YGgESsrR2e4JOeP2qtmQo2S8BjI+Y/VxPQ6WbNFzAkXxDniHcnPCrhkeX36SKINvMjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.com\"}" https://proxy-host.com/auth/verifykey -``` - -* 200 {"info":"4096 SHA256:dfdCYzt0atMBXVZTJzUxsu99IjXXFXpocSox5q+jOs8 wmantly@gmail.com (RSA)\n"} -* 400 {"message":"Key is not a public key file!"} - - -## add ssh key to current user - -**post** `/users/key` - -```bash -curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d "{\"key\": \"ssh-rsa AAAAB3NzaC1yc2EAAjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.co\"}" https://proxy-host.com/users/key -``` - -* 200 {"message":true} -* 400 {"message":"Bad SSH key"} diff --git a/nodejs/routes/auth.js b/nodejs/routes/auth.js index bc70c70..0e126a8 100755 --- a/nodejs/routes/auth.js +++ b/nodejs/routes/auth.js @@ -66,7 +66,7 @@ router.post('/resetpassword', rateLimit.passwordReset, async function(req, res, console.info('resetpassword for', req.body.mail, 'sent') return res.json({ - message: 'If the emaill address is in our system, you will receive a message.' + message: 'If the email address is in our system, you will receive a message.' }); }catch(error){ next(error); diff --git a/tos.md b/tos.md index 45a3754..471532d 100644 --- a/tos.md +++ b/tos.md @@ -2,19 +2,27 @@ *Last updated: June 2026* -Welcome to Theta42. By creating an account and using any services on this system, you agree to the following terms. Please read them carefully — they're short and written in plain English. +> **This is a template.** SSO Manager ships this file as a starting point for +> operators to adapt to their own deployment, organization name, and +> jurisdiction. Replace the placeholder text below (or the whole document) +> with terms reviewed by your own admin/legal before relying on it. See +> [issue #39](https://github.com/theta42/sso-manager-node/issues/39) for the +> planned admin UI that will let operators edit this document without a code +> change. + +Welcome. By creating an account and using any services on this system, you agree to the following terms. Please read them carefully — they're short and written in plain English. --- ## 1. Who This Applies To -These terms apply to anyone with an account on Theta42 systems, including but not limited to: SSH access, Emby, Gitea, self-managed containers or VMs, Proxmox, and any other services accessible with your credentials. +These terms apply to anyone with an account on this system, including but not limited to: SSH access, self-managed containers or VMs, and any other services accessible with your credentials. --- ## 2. Acceptable Use -- **Be respectful.** Treat other users and the admin with respect. Harassment, abuse, or intentionally disrupting other users' work is not tolerated. +- **Be respectful.** Treat other users and administrators with respect. Harassment, abuse, or intentionally disrupting other users' work is not tolerated. - **Do no harm.** Do not attempt to access, modify, damage, or disrupt any system, service, or data you are not explicitly authorized to use — including systems outside this network. - **No malicious activity.** Do not use your account to run port scans, exploits, denial-of-service attacks, spam campaigns, or any other activity intended to harm others. - **No resource abuse.** Do not use shared infrastructure for cryptocurrency mining, bulk email, or other activities that consume excessive CPU, memory, disk, or bandwidth without prior approval. @@ -23,36 +31,36 @@ These terms apply to anyone with an account on Theta42 systems, including but no ## 3. Legal Compliance -- You agree to comply with all applicable **United States federal law** and the laws of **your current jurisdiction**. -- Do not store, transmit, access, or distribute any content that is illegal under US law or the laws where you are located. +- You agree to comply with all applicable laws in your jurisdiction and the jurisdiction where this service is operated. +- Do not store, transmit, access, or distribute any content that is illegal under applicable law. - This includes but is not limited to: pirated software, copyrighted content you do not have rights to, and any material that is illegal in your jurisdiction. --- ## 4. Credential Security -- Do not share your password, SSH keys, or any other credentials with anyone — including the admin (who will never ask for your password). +- Do not share your password, SSH keys, or any other credentials with anyone — including administrators (who will never ask for your password). - You are responsible for all activity that occurs under your account. -- If you suspect your account has been compromised, notify the admin immediately and change your password. +- If you suspect your account has been compromised, notify an administrator immediately and change your password. --- ## 5. Self-Managed Resources -Users with access to containers, virtual machines, or Proxmox agree to additional responsibilities: +Users with access to containers or virtual machines agree to additional responsibilities: - You are **fully responsible** for everything you run inside your allocated resources. - Keep your systems patched and reasonably secured. A compromised VM on the network is everyone's problem. - Do not use your allocation to attack, scan, or probe other systems — on this network or anywhere else. - Resource abuse (running workloads that harm shared infrastructure or network performance) may result in immediate suspension of your allocation. -- Do not expose services to the internet without coordinating with the admin. +- Do not expose services to the internet without coordinating with an administrator. --- ## 6. Privacy - Your personal data (name, email address, phone number) is stored solely to operate this service. It will not be sold, rented, or shared with third parties. -- **The admin reserves the right to inspect** activity logs, files, running processes, and system usage on shared infrastructure at any time — for security purposes, incident investigation, or to verify compliance with these terms. +- **Administrators reserve the right to inspect** activity logs, files, running processes, and system usage on shared infrastructure at any time — for security purposes, incident investigation, or to verify compliance with these terms. - **You have no expectation of privacy** on systems, storage, or network traffic hosted on this infrastructure. --- @@ -60,7 +68,7 @@ Users with access to containers, virtual machines, or Proxmox agree to additiona ## 7. Account Termination - Violation of any of these terms may result in **immediate account suspension or deletion without prior notice**. -- The admin reserves sole discretion to terminate access for any reason. +- Administrators reserve sole discretion to terminate access for any reason. --- @@ -72,4 +80,4 @@ This agreement may be updated at any time. The current version is always availab ## Questions? -Reach the admin through the SSO manager or through the associated communication channels (Discord, Signal, etc.). +Reach an administrator through the SSO manager or your organization's usual support channel.