fix(authentik): provision the Audiobookshelf provider with grant types and OpenID scopes #129

Merged
dank merged 3 commits from fix/abs-grant-types into main 2026-11-02 20:24:53 -08:00
Owner

Symptom

Sign in with Authentik bounced every Audiobookshelf login back to /login?error=…, even after #128 fixed the base path and the redirect subfolder. Two independent faults were sitting in the authentik provider row that scripts/authentik-audiobookshelf-sso.sh creates, and neither one says its own name on the wire.

  1. grant_types was []. authentik 2026.8 defaults that field to the empty list for a provider created through the ORM, so /application/o/authorize/ refused the code flow with oauthlib's generic The request is otherwise malformed. The only line that names the cause is the server log's Invalid grant_type for provider. The redirect-URI check runs first, so while the URI was also wrong (fixed in #128) the browser showed only "Redirect URI Error"; fixing the URI unveiled this one.
  2. property_mappings carried only the abs_groups claim mapping. The granted token's scope is the intersection of the provider's available scopes and what the client asked for (openid profile email abs_groups), so with only abs_groups on the provider the token came back short. The code was issued and exchanged normally, and ABS's follow-up /userinfo call then died insufficient_scope (The request requires higher privileges than provided by the access token) — a fault that reads like an Audiobookshelf bug and is an IdP row.

Both were fixed live during the investigation; this branch is the encoding, so a blueprint re-import or an authentik rebuild restores the working row instead of silently restoring the dead one.

Change

scripts/authentik-audiobookshelf-sso.sh — create and reconcile paths:

  • grant_types = ["authorization_code", "refresh_token"] (refresh because ABS runs authOpenIDRefreshEnabled).
  • property_mappings = the three built-in OpenID mappings (openid, profile, email) plus the claim mapping; a drifted provider gets re-set, and a provider missing one of the built-ins prints which.
  • provisions qa-e2e-admin into audiobookshelf_admins — the identity the new gate signs in as (ABS_E2E_ADMIN overrides).
  • the verification print now shows the grant list and the scope set, so a re-run is the census.

tests/e2e/specs/23-audiobookshelf-sso.spec.ts, joined to the admin project so it rides every deploy.sh e2e gate. This service answers on 192.168.0.235:8000 with no DNS name and no vhost, so it is in no guarded-vhost list and no spec had ever fetched a byte of it — which is why both defects survived a green deploy. It asserts the two consumer-visible outcomes:

  • every .js the SPA loads arrives as JavaScript — a bundle built for the wrong base path comes back 200 text/html, the page never leaves "Loading…", and /healthcheck is perfectly happy;
  • /api/me answers 200 with type: "admin" — true only while grant types, the OpenID scope mappings and the abs_groups claim hold together.

serverAppConfigs-docs/Dankserver/Audiobookshelf.md — the two laws with their failure signatures, the admin-group fixture row, and the live gate named under Verification.

Evidence

Re-ran the script against the live IdP — the repo script now reproduces exactly the working row (no drift left to re-break):

mapping: already current
provider: asserted (nothing to change)
e2e fixture: qa-e2e-admin in audiobookshelf_admins
admin: akadmin in audiobookshelf_admins
claim for qa-e2e-admin -> {'abs_groups': ['admin', 'user']}
provider audiobookshelf | client_id audiobookshelf | flow default-provider-authorization-implicit-consent | grants ['authorization_code', 'refresh_token'] | uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', 'http://192.168.0.235:8000/audiobookshelf/auth/openid/mobile-redirect']
scopes: ['abs_groups', 'email', 'openid', 'profile']

The new gate against the live service:

✓ [setup] 00-auth.setup.ts › login qa-e2e (23.3s)
✓ [setup] 00-auth.setup.ts › login qa-e2e-admin (23.2s)
✓ [admin] 23-audiobookshelf-sso.spec.ts › SSO round trip boots the SPA and grants a real Audiobookshelf session (2.9s)
3 passed (50.8s)

./tests/run.sh: 824/824 — e2e_scaffold accepts the new spec number (an unowned prefix silently never runs), docs completeness/link integrity accept the page edits.

Red-capability is this incident, not a simulation: while the provider was broken the round trip never leaves /login (the URL poll fails at 90 s), and before #128 the script requests came back text/html (the MIME assertion fails). Two spec drafts died on their own bugs on the way — an invented #app mount selector (the shipped shell mounts at #__nuxt, and its static loading markup proves nothing, so the visible SSO button is the render proof) and a single-shot localStorage read that raced the callback (now a 60 s poll over localStorage and the abs_token cookie).

Rollback

The test and docs commits revert cleanly. The IdP half is not a git revert: reverting the script does not touch the live row, and re-running the reverted script would strip grant_types and re-break the login. Rolling the IdP back would mean a live write (grant_types → []); nobody wants that — the local frank break-glass account stays enabled throughout either way.

Open decision

audiobookshelf_admins currently holds akadmin and qa-e2e-admin — not frank. With authOpenIDAutoRegister on and the fail-closed mapping, your own Authentik sign-in is refused unless you are in one of the two groups. One line in the script (or ABS_OIDC_USERS="frank" for the user tier) plus a re-run, if you want it.


cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb

## Symptom `Sign in with Authentik` bounced every Audiobookshelf login back to `/login?error=…`, even after #128 fixed the base path and the redirect subfolder. Two independent faults were sitting in the authentik provider row that `scripts/authentik-audiobookshelf-sso.sh` creates, and neither one says its own name on the wire. 1. **`grant_types` was `[]`.** authentik 2026.8 defaults that field to the empty list for a provider created through the ORM, so `/application/o/authorize/` refused the code flow with oauthlib's generic `The request is otherwise malformed`. The only line that names the cause is the server log's `Invalid grant_type for provider`. The redirect-URI check runs *first*, so while the URI was also wrong (fixed in #128) the browser showed only "Redirect URI Error"; fixing the URI unveiled this one. 2. **`property_mappings` carried only the `abs_groups` claim mapping.** The granted token's scope is the intersection of the provider's available scopes and what the client asked for (`openid profile email abs_groups`), so with only `abs_groups` on the provider the token came back short. The code was issued and exchanged normally, and ABS's follow-up `/userinfo` call then died `insufficient_scope (The request requires higher privileges than provided by the access token)` — a fault that reads like an Audiobookshelf bug and is an IdP row. Both were fixed live during the investigation; this branch is the encoding, so a blueprint re-import or an authentik rebuild restores the working row instead of silently restoring the dead one. ## Change `scripts/authentik-audiobookshelf-sso.sh` — create **and** reconcile paths: * `grant_types = ["authorization_code", "refresh_token"]` (refresh because ABS runs `authOpenIDRefreshEnabled`). * `property_mappings` = the three built-in OpenID mappings (`openid`, `profile`, `email`) **plus** the claim mapping; a drifted provider gets re-set, and a provider missing one of the built-ins prints which. * provisions `qa-e2e-admin` into `audiobookshelf_admins` — the identity the new gate signs in as (`ABS_E2E_ADMIN` overrides). * the verification print now shows the grant list and the scope set, so a re-run is the census. `tests/e2e/specs/23-audiobookshelf-sso.spec.ts`, joined to the `admin` project so it rides every `deploy.sh` e2e gate. This service answers on `192.168.0.235:8000` with no DNS name and no vhost, so it is in no guarded-vhost list and **no spec had ever fetched a byte of it** — which is why both defects survived a green deploy. It asserts the two consumer-visible outcomes: * every `.js` the SPA loads arrives as JavaScript — a bundle built for the wrong base path comes back `200 text/html`, the page never leaves "Loading…", and `/healthcheck` is perfectly happy; * `/api/me` answers 200 with `type: "admin"` — true only while grant types, the OpenID scope mappings and the `abs_groups` claim hold together. `serverAppConfigs-docs/Dankserver/Audiobookshelf.md` — the two laws with their failure signatures, the admin-group fixture row, and the live gate named under *Verification*. ## Evidence Re-ran the script against the live IdP — the repo script now reproduces exactly the working row (no drift left to re-break): ``` mapping: already current provider: asserted (nothing to change) e2e fixture: qa-e2e-admin in audiobookshelf_admins admin: akadmin in audiobookshelf_admins claim for qa-e2e-admin -> {'abs_groups': ['admin', 'user']} provider audiobookshelf | client_id audiobookshelf | flow default-provider-authorization-implicit-consent | grants ['authorization_code', 'refresh_token'] | uris ['http://192.168.0.235:8000/audiobookshelf/auth/openid/callback', 'http://192.168.0.235:8000/audiobookshelf/auth/openid/mobile-redirect'] scopes: ['abs_groups', 'email', 'openid', 'profile'] ``` The new gate against the live service: ``` ✓ [setup] 00-auth.setup.ts › login qa-e2e (23.3s) ✓ [setup] 00-auth.setup.ts › login qa-e2e-admin (23.2s) ✓ [admin] 23-audiobookshelf-sso.spec.ts › SSO round trip boots the SPA and grants a real Audiobookshelf session (2.9s) 3 passed (50.8s) ``` `./tests/run.sh`: 824/824 — `e2e_scaffold` accepts the new spec number (an unowned prefix silently never runs), docs completeness/link integrity accept the page edits. Red-capability is this incident, not a simulation: while the provider was broken the round trip never leaves `/login` (the URL poll fails at 90 s), and before #128 the script requests came back `text/html` (the MIME assertion fails). Two spec drafts died on their own bugs on the way — an invented `#app` mount selector (the shipped shell mounts at `#__nuxt`, and its static loading markup proves nothing, so the visible SSO button is the render proof) and a single-shot localStorage read that raced the callback (now a 60 s poll over localStorage and the `abs_token` cookie). ## Rollback The test and docs commits revert cleanly. The IdP half is **not** a git revert: reverting the script does not touch the live row, and re-running the reverted script would strip `grant_types` and re-break the login. Rolling the IdP back would mean a live write (`grant_types` → `[]`); nobody wants that — the local `frank` break-glass account stays enabled throughout either way. ## Open decision `audiobookshelf_admins` currently holds `akadmin` and `qa-e2e-admin` — **not** `frank`. With `authOpenIDAutoRegister` on and the fail-closed mapping, your own Authentik sign-in is refused unless you are in one of the two groups. One line in the script (or `ABS_OIDC_USERS="frank"` for the user tier) plus a re-run, if you want it. --- cwd: /home/blank/.omp/wt/wt-20261030-163847-6d1fc1f · session: 01a18f2e-18b1-7573-bcd5-ac99636f8bcb
dank added 3 commits 2026-11-02 20:21:52 -08:00
The provider row this script creates was missing two fields, and each one is
a dead login that names nothing on the wire (both measured 2026-11-02 in the
browser):

* grant_types defaults to [] on an ORM-created provider in authentik 2026.8,
  so /application/o/authorize/ refused the code flow with oauthlib's generic
  'The request is otherwise malformed'; the log line that names the cause is
  'Invalid grant_type for provider'. Set authorization_code + refresh_token,
  and reconcile it on re-run.
* property_mappings held only the abs_groups claim mapping, so the granted
  token was scoped shorter than the scopes ABS requests and ABS's /userinfo
  call died 'insufficient_scope' after a good code exchange. Attach the
  built-in OpenID openid/profile/email mappings beside the claim.

Also provisions qa-e2e-admin into the admin group: it is the identity the new
Playwright gate signs in as, so a blueprint re-import restores the fixture.
The service answers on a LAN address with no vhost, so it was in no
guarded-vhost list and no spec touched it: every deploy gate passed while its
login was dead. The gate signs in as qa-e2e-admin and asserts the two
consumer-visible outcomes that the two defect classes break - every .js the
SPA loads arrives as JavaScript (a bundle built for the wrong base path comes
back text/html and boots nothing), and /api/me answers 200 with type 'admin'
(true only while grant types, the OpenID scope mappings and the group claim
all hold). Joined to the admin project, so it rides every deploy e2e gate.
Records that an ORM-created provider has no grant types and that the claim
mapping is not the whole property_mappings list, with the failure signatures
to recognise; names qa-e2e-admin as the admin-group fixture and the new
Playwright spec as the live verification.
dank merged commit 80a65b013f into main 2026-11-02 20:24:53 -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#129