From 30186d8365096c2b90b7839f48bee68ef39e9e80 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 3 Nov 2026 02:06:20 -0800 Subject: [PATCH] feat(authentik): make every Audiobookshelf SSO signer a listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The abs_groups claim decided the listener role from membership of audiobookshelf_users, a group nobody was ever added to, while the ABS application carries no access policy of its own. The result: seventeen household accounts authenticated at the IdP successfully and were then refused by the app — a policy decision that reads as a broken login. The claim now answers ["user"] for every signer and appends "admin" only for audiobookshelf_admins, so the IdP login is the gate and onboarding a listener is exactly "create the authentik account". Admins keep the exact claim they always received, ["admin","user"], in that order: ABS's group-tag matching has never been measured for order-sensitivity and the working admin login is not where to find out. audiobookshelf_users is deleted rather than left empty — a group that decides nothing is a trap for the next reader, who will assume it gates something. Two side fixes found by running the script: - Two comments inside the unquoted PYEOF heredoc carried backticks, so bash command-substituted them locally on EVERY run ("audiobookshelf_users: command not found", "user: command not found") and spliced the output into the python program. Names in that body are plain now, with a note on why. A quoted heredoc is not an option: the body interpolates. - The default-grant probe picked authentik's AnonymousUser row, then its admin superuser. It now walks real non-superuser accounts, preferring the fleet's plain-user marker group, and prints up to three claims. --- scripts/audiobookshelf-init.sh | 6 ++- scripts/authentik-audiobookshelf-sso.sh | 72 +++++++++++++++++-------- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/scripts/audiobookshelf-init.sh b/scripts/audiobookshelf-init.sh index f4ac4c1..5d81f78 100755 --- a/scripts/audiobookshelf-init.sh +++ b/scripts/audiobookshelf-init.sh @@ -268,8 +268,10 @@ else: # not auto-launch: the login page keeps offering the local form, which is the # break-glass path when the IdP is unreachable. "authOpenIDAutoLaunch": False, - # a household user's first SSO sign-in creates their account; the group claim - # decides the role, and a user in neither group is denied (fail closed). + # a household user's first SSO sign-in creates their account. The role comes from + # the abs_groups claim, which grants the listener role to every signer; only the + # authentik admin group escalates. Revoking a listener therefore happens HERE in + # ABS (disable or delete the user), never by removing an IdP group membership. "authOpenIDAutoRegister": True, "authOpenIDGroupClaim": "abs_groups", # The app's own scheme lives HERE, never in the IdP: ABS swaps it for diff --git a/scripts/authentik-audiobookshelf-sso.sh b/scripts/authentik-audiobookshelf-sso.sh index d7b73de..e163b50 100755 --- a/scripts/authentik-audiobookshelf-sso.sh +++ b/scripts/authentik-audiobookshelf-sso.sh @@ -70,9 +70,12 @@ # ABS_OIDC_CLIENT_ID provider/client id (audiobookshelf) # ABS_OIDC_GROUPS_CLAIM claim ABS reads for roles (abs_groups) # ABS_OIDC_USERNAME authentik user made admin (akadmin) -# ABS_OIDC_USERS space-separated authentik usernames given the USER group -# (default: nobody — an empty list is fail-closed, and the -# household decides who listens to audiobooks) +# There is deliberately NO user list. The claim grants the LISTENER role to every +# authentik account that signs in (2026-11-03), so onboarding a listener is exactly +# "create the authentik account". Before this, a household account authenticated at +# the IdP and was then refused by ABS for being in no group — a policy decision that +# read as a broken login. Revocation lives on the ABS side (disable or delete the +# user there): a group can no longer withhold a role the claim always sends. # ABS_E2E_ADMIN authentik user kept in the ADMIN group for the Playwright gate # (qa-e2e-admin — tests/e2e/specs/23-audiobookshelf-sso.spec.ts # signs in as this identity, so a re-run restores the fixture) @@ -97,53 +100,67 @@ SECRET="${ABS_OIDC_CLIENT_SECRET:?set ABS_OIDC_CLIENT_SECRET (see the header)}" CLIENT_ID="${ABS_OIDC_CLIENT_ID:-audiobookshelf}" CLAIM="${ABS_OIDC_GROUPS_CLAIM:-abs_groups}" ADMIN_GROUP="audiobookshelf_admins" -USER_GROUP="audiobookshelf_users" SSO_USER="${ABS_OIDC_USERNAME:-akadmin}" -MEMBERS="${ABS_OIDC_USERS:-}" E2E_ADMIN="${ABS_E2E_ADMIN:-qa-e2e-admin}" BASE="${ABS_BASE:-http://192.168.0.235:8000/audiobookshelf}" EXTRA_BASES="${ABS_EXTRA_BASES:-https://audiobooks.franksdank.cloud/audiobookshelf}" HOST="${AUTHENTIK_HOST:-dankclaw}" -# The claim expression: translate group membership into ABS role names. It is stored -# verbatim so a later read of the provider shows exactly this policy. +# The claim expression: every signer is a listener; only the admin group escalates. +# Stored verbatim so a later read of the provider shows exactly this policy. The default +# lives HERE and nowhere else — ABS reads this list off the token, so an account in no +# Audiobookshelf group still arrives with ["user"] and is auto-registered as a listener +# instead of being refused after a successful authentication. read -r -d '' EXPR < %s" % ( + u.username, sorted(g.name for g in u.groups.all()), + _ns["_probe"](user=u, request=None))) # --- verification: the claim the callback will actually receive --------------------- -for probe in [root] + ([e2e] if e2e else []) + [User.objects.get(username=n) for n in MEMBERS]: +for probe in [root] + ([e2e] if e2e else []): print("claim for %s -> %s" % (probe.username, _ns["_probe"](user=probe, request=None))) p.refresh_from_db() print("provider %s | client_id %s | flow %s | grants %s | uris %s" % (