docs(audiobookshelf): /api/login is not a route — and what auto-registration grants #135

Merged
dank merged 1 commits from docs/abs-login-path-and-listener-shape into main 2026-11-03 03:04:05 -08:00
Owner

Why

Yesterday I reported that the recorded Audiobookshelf break-glass credential was stale —
that it answered 401 and so local might not be the rescue path the vault promises when the
IdP is down. That conclusion was wrong, and the vault text that led to it is wrong in the same
way: POST /api/login is not a route.

Local login is POST <base>/login (what scripts/audiobookshelf-init.sh line 184 has always
used). Anything under /api goes through the bearer guard first, and that guard answers a bare
401 Unauthorized and logs nothing — no [LocalAuth] line — so a wrong path is
indistinguishable from a wrong password unless the journal is read alongside it. That is the
same family of trap the init script already documents for the absent /setup route, and the
page even warned about a 401 "as though the password were wrong" one paragraph away, for a
different cause.

Evidence

  • bcrypt.compare(file password, users.pash) → true (run with bcryptjs from the pinned
    image, credential and hash passed by env, never argv). The recorded value is the live root
    password; break-glass works.
  • The state DB agrees: frank is type=root with createdAt == updatedAt
    (2026-11-02 10:36 UTC) — nobody ever rotated a password, so no rotation could have
    invalidated the file.
  • The journal shows the credential working two minutes after the file was written:
    19:20:13 [LocalAuth] User "frank" logged in then 19:21:11 Enabling active auth method "openid".
  • POST /audiobookshelf/login with the file credential → 200, token at user.token,
    type=root. The two failing calls in my earlier report hit /api/login and produced no
    LocalAuth log line at all.
  • serverVersion from /status is 2.37.2, so the version claim on the page stands.

What the correct census answers

The item I had to leave [UNVERIFIED] — what an auto-registered listener can actually see — is
now measured from the state DB (the API's GET /api/users returns the limited shape: no
active, no librariesAccessible, which is what produced my earlier misreadings):

Account type local password grants
frank root yes all libraries
qa-e2e user none (pash NULL) accessAllLibraries: true, download: true
qa-e2e-admin admin none all libraries
akadmin admin none all libraries

So every household signer who uses SSO arrives with the whole library and offline downloads
already on
, and carries no local password at all; its IdP link lives at
extraData.authOpenIDSub. Narrowing is per account in Settings → Users → <name>.

Changes — one page, four fixes

  • Bootstrap: the recorded value is the live password (bcrypt-compared against users.pash),
    so local does open the door when authentik is down.
  • Phone app: new paragraph naming /api/login as a non-route, the silent-401 signature,
    and the offline way to settle a suspected stale credential without signing in.
  • Listeners: what auto-registration hands out (accessAllLibraries: true,
    download: true), and the note that a missing key in the limited API shape is not a denial.
  • Listeners: the rate-limit line protected /audiobookshelf/api/login — a path that can
    never carry a login. It now names POST /audiobookshelf/login, the form that is actually
    exposed.

No behavior change: no script, unit or spec is touched. The four lines exist so the next agent
does not re-derive this by concluding a working door is dead.

Static suite: ./tests/run.sh on the branch → 824 passed, ALL TESTS PASSED, including
the docs-completeness and vault-link-integrity seams that gate every page edit.


cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
Build worktree: /tmp/wt-ssogrant (branch docs/abs-login-path-and-listener-shape, off origin/main d3aedd1).

## Why Yesterday I reported that the recorded Audiobookshelf break-glass credential was **stale** — that it answered `401` and so `local` might not be the rescue path the vault promises when the IdP is down. That conclusion was wrong, and the vault text that led to it is wrong in the same way: **`POST /api/login` is not a route.** Local login is `POST <base>/login` (what `scripts/audiobookshelf-init.sh` line 184 has always used). Anything under `/api` goes through the bearer guard first, and that guard answers a bare `401 Unauthorized` **and logs nothing** — no `[LocalAuth]` line — so a wrong path is indistinguishable from a wrong password unless the journal is read alongside it. That is the same family of trap the init script already documents for the absent `/setup` route, and the page even warned about a `401` "as though the password were wrong" one paragraph away, for a different cause. ## Evidence - `bcrypt.compare(file password, users.pash)` → **`true`** (run with `bcryptjs` from the pinned image, credential and hash passed by env, never argv). The recorded value *is* the live root password; break-glass works. - The state DB agrees: `frank` is `type=root` with `createdAt == updatedAt` (2026-11-02 10:36 UTC) — nobody ever rotated a password, so no rotation could have invalidated the file. - The journal shows the credential working two minutes after the file was written: `19:20:13 [LocalAuth] User "frank" logged in` then `19:21:11 Enabling active auth method "openid"`. - `POST /audiobookshelf/login` with the file credential → **`200`**, token at `user.token`, `type=root`. The two failing calls in my earlier report hit `/api/login` and produced no `LocalAuth` log line at all. - `serverVersion` from `/status` is **2.37.2**, so the version claim on the page stands. ## What the correct census answers The item I had to leave `[UNVERIFIED]` — what an auto-registered listener can actually see — is now measured from the state DB (the API's `GET /api/users` returns the *limited* shape: no `active`, no `librariesAccessible`, which is what produced my earlier misreadings): | Account | type | local password | grants | |---|---|---|---| | `frank` | root | yes | all libraries | | `qa-e2e` | user | none (`pash` NULL) | `accessAllLibraries: true`, `download: true` | | `qa-e2e-admin` | admin | none | all libraries | | `akadmin` | admin | none | all libraries | So every household signer who uses SSO arrives with the **whole library and offline downloads already on**, and carries no local password at all; its IdP link lives at `extraData.authOpenIDSub`. Narrowing is per account in **Settings → Users → \<name\>**. ## Changes — one page, four fixes - **Bootstrap**: the recorded value is the live password (bcrypt-compared against `users.pash`), so `local` does open the door when authentik is down. - **Phone app**: new paragraph naming `/api/login` as a non-route, the silent-`401` signature, and the offline way to settle a suspected stale credential without signing in. - **Listeners**: what auto-registration hands out (`accessAllLibraries: true`, `download: true`), and the note that a missing key in the limited API shape is not a denial. - **Listeners**: the rate-limit line protected `/audiobookshelf/api/login` — a path that can never carry a login. It now names `POST /audiobookshelf/login`, the form that is actually exposed. No behavior change: no script, unit or spec is touched. The four lines exist so the next agent does not re-derive this by concluding a working door is dead. Static suite: `./tests/run.sh` on the branch → **824 passed**, `ALL TESTS PASSED`, including the docs-completeness and vault-link-integrity seams that gate every page edit. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb Build worktree: `/tmp/wt-ssogrant` (branch `docs/abs-login-path-and-listener-shape`, off `origin/main` `d3aedd1`).
dank added 1 commit 2026-11-03 02:28:32 -08:00
A wrong path was mistaken for a dead break-glass credential. Local login is
POST <base>/login; /api/* is behind the bearer guard, which answers a bare 401
and logs nothing, so a bad path looks exactly like a bad password. The recorded
value bcrypt-matches users.pash, so local really is the door when authentik is
down, and the rate-limit line was protecting a path that cannot carry a login.

Also records what auto-registration actually grants, measured from the state DB
because GET /api/users returns the limited shape: every SSO signer arrives with
accessAllLibraries and download already on, and carries no local password.
dank merged commit 1a47b831ee into main 2026-11-03 03:04:05 -08:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dank/serverAppConfigs#135