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 Unauthorizedand 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.
## 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`).
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 main2026-11-03 03:04:05 -08:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Why
Yesterday I reported that the recorded Audiobookshelf break-glass credential was stale —
that it answered
401and solocalmight not be the rescue path the vault promises when theIdP is down. That conclusion was wrong, and the vault text that led to it is wrong in the same
way:
POST /api/loginis not a route.Local login is
POST <base>/login(whatscripts/audiobookshelf-init.shline 184 has alwaysused). Anything under
/apigoes through the bearer guard first, and that guard answers a bare401 Unauthorizedand logs nothing — no[LocalAuth]line — so a wrong path isindistinguishable 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
/setuproute, and thepage even warned about a
401"as though the password were wrong" one paragraph away, for adifferent cause.
Evidence
bcrypt.compare(file password, users.pash)→true(run withbcryptjsfrom the pinnedimage, credential and hash passed by env, never argv). The recorded value is the live root
password; break-glass works.
frankistype=rootwithcreatedAt == updatedAt(2026-11-02 10:36 UTC) — nobody ever rotated a password, so no rotation could have
invalidated the file.
19:20:13 [LocalAuth] User "frank" logged inthen19:21:11 Enabling active auth method "openid".POST /audiobookshelf/loginwith the file credential →200, token atuser.token,type=root. The two failing calls in my earlier report hit/api/loginand produced noLocalAuthlog line at all.serverVersionfrom/statusis 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 — isnow measured from the state DB (the API's
GET /api/usersreturns the limited shape: noactive, nolibrariesAccessible, which is what produced my earlier misreadings):frankqa-e2epashNULL)accessAllLibraries: true,download: trueqa-e2e-adminakadminSo 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
users.pash),so
localdoes open the door when authentik is down./api/loginas a non-route, the silent-401signature,and the offline way to settle a suspected stale credential without signing in.
accessAllLibraries: true,download: true), and the note that a missing key in the limited API shape is not a denial./audiobookshelf/api/login— a path that cannever carry a login. It now names
POST /audiobookshelf/login, the form that is actuallyexposed.
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.shon the branch → 824 passed,ALL TESTS PASSED, includingthe 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(branchdocs/abs-login-path-and-listener-shape, offorigin/maind3aedd1).