fix(abs-guard): audiobooks.franksdank.cloud completes OIDC (forwarded scheme + provider OAuth defaults) #131
No files matched your search
+7
-6
@@ -268,10 +268,11 @@ files.franksdank.cloud {
|
|||||||
# Anubis → Authentik → app, like romm/files. The container carries
|
# Anubis → Authentik → app, like romm/files. The container carries
|
||||||
# ROUTER_BASE_PATH=/audiobookshelf, so this vhost serves the app under that
|
# ROUTER_BASE_PATH=/audiobookshelf, so this vhost serves the app under that
|
||||||
# path; the bare root is redirected onto it in the post-auth listener, never
|
# path; the bare root is redirected onto it in the post-auth listener, never
|
||||||
# pre-auth. Guard state in the IdP: proxy provider `abs-proxy`
|
# pre-auth. Guard state in the IdP: proxy provider `audiobooks-proxy`
|
||||||
# (external_host https://abs.franksdank.cloud/, internal_host http://caddy:8017,
|
# (external_host https://audiobooks.franksdank.cloud/, internal_host
|
||||||
# application "Audiobookshelf Guard"), claimed by the embedded outpost.
|
# http://caddy:8017, application "Audiobookshelf Guard"), claimed by the
|
||||||
abs.franksdank.cloud {
|
# embedded outpost.
|
||||||
|
audiobooks.franksdank.cloud {
|
||||||
reverse_proxy anubis:8923 {
|
reverse_proxy anubis:8923 {
|
||||||
header_up X-Real-Ip {remote_host}
|
header_up X-Real-Ip {remote_host}
|
||||||
}
|
}
|
||||||
@@ -423,8 +424,8 @@ http://:8017 {
|
|||||||
reverse_proxy 192.168.0.235:3923
|
reverse_proxy 192.168.0.235:3923
|
||||||
}
|
}
|
||||||
|
|
||||||
@abs host abs.franksdank.cloud
|
@audiobooks host audiobooks.franksdank.cloud
|
||||||
handle @abs {
|
handle @audiobooks {
|
||||||
# The app answers only under its ROUTER_BASE_PATH; the bare root is a
|
# The app answers only under its ROUTER_BASE_PATH; the bare root is a
|
||||||
# convenience hop, taken post-auth so nothing reaches the app
|
# convenience hop, taken post-auth so nothing reaches the app
|
||||||
# pre-authentication.
|
# pre-authentication.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
192.168.0.143 romm.franksdank.cloud
|
192.168.0.143 romm.franksdank.cloud
|
||||||
192.168.0.143 files.franksdank.cloud
|
192.168.0.143 files.franksdank.cloud
|
||||||
192.168.0.143 jelly.franksdank.cloud
|
192.168.0.143 jelly.franksdank.cloud
|
||||||
192.168.0.143 abs.franksdank.cloud
|
192.168.0.143 audiobooks.franksdank.cloud
|
||||||
192.168.0.143 radarr.franksdank.cloud
|
192.168.0.143 radarr.franksdank.cloud
|
||||||
192.168.0.143 lidarr.franksdank.cloud
|
192.168.0.143 lidarr.franksdank.cloud
|
||||||
192.168.0.143 readarr.franksdank.cloud
|
192.168.0.143 readarr.franksdank.cloud
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
# origin, reached directly, which is also what the phone
|
# origin, reached directly, which is also what the phone
|
||||||
# app is configured with)
|
# app is configured with)
|
||||||
# ABS_EXTRA_BASES space-separated additional origins, base path included
|
# ABS_EXTRA_BASES space-separated additional origins, base path included
|
||||||
# (default: https://abs.franksdank.cloud/audiobookshelf —
|
# (default: https://audiobooks.franksdank.cloud/audiobookshelf
|
||||||
# the guarded vhost, dankclaw/Caddyfile).
|
# — the guarded vhost, dankclaw/Caddyfile).
|
||||||
# ABS builds its redirect_uri from the ORIGIN of the
|
# ABS builds its redirect_uri from the ORIGIN of the
|
||||||
# request, so each origin the app is served at needs its
|
# request, so each origin the app is served at needs its
|
||||||
# own registered URI — an unregistered origin is a
|
# own registered URI — an unregistered origin is a
|
||||||
@@ -100,7 +100,7 @@ SSO_USER="${ABS_OIDC_USERNAME:-akadmin}"
|
|||||||
MEMBERS="${ABS_OIDC_USERS:-}"
|
MEMBERS="${ABS_OIDC_USERS:-}"
|
||||||
E2E_ADMIN="${ABS_E2E_ADMIN:-qa-e2e-admin}"
|
E2E_ADMIN="${ABS_E2E_ADMIN:-qa-e2e-admin}"
|
||||||
BASE="${ABS_BASE:-http://192.168.0.235:8000/audiobookshelf}"
|
BASE="${ABS_BASE:-http://192.168.0.235:8000/audiobookshelf}"
|
||||||
EXTRA_BASES="${ABS_EXTRA_BASES:-https://abs.franksdank.cloud/audiobookshelf}"
|
EXTRA_BASES="${ABS_EXTRA_BASES:-https://audiobooks.franksdank.cloud/audiobookshelf}"
|
||||||
HOST="${AUTHENTIK_HOST:-dankclaw}"
|
HOST="${AUTHENTIK_HOST:-dankclaw}"
|
||||||
|
|
||||||
# The claim expression: translate group membership into ABS role names. It is stored
|
# The claim expression: translate group membership into ABS role names. It is stored
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
#
|
#
|
||||||
# * redirect_uris empty -> authentik answers its own page:
|
# * redirect_uris empty -> authentik answers its own page:
|
||||||
# "Redirect URI Error — The request fails due to a missing, invalid, or
|
# "Redirect URI Error — The request fails due to a missing, invalid, or
|
||||||
# mismatching redirection URI (redirect_uri)". Measured on abs-proxy
|
# mismatching redirection URI (redirect_uri)". Measured on the Audiobookshelf
|
||||||
# (abs.franksdank.cloud) 2026-11-03.
|
# guard provider, 2026-11-03.
|
||||||
# * grant_types empty -> the authorize endpoint refuses the outpost's
|
# * grant_types empty -> the authorize endpoint refuses the outpost's
|
||||||
# authorization-code request with a bodiless HTTP 400 (Chromium renders
|
# authorization-code request with a bodiless HTTP 400 (Chromium renders
|
||||||
# "This page isn't working / HTTP ERROR 400"). Same provider, one step
|
# "This page isn't working / HTTP ERROR 400"). Same provider, one step
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on
|
|||||||
array.
|
array.
|
||||||
|
|
||||||
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) —
|
**Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user manager) —
|
||||||
**Doors:** LAN `http://192.168.0.235:8000/audiobookshelf` (direct, what the phone app uses) and guarded `https://abs.franksdank.cloud/audiobookshelf/`.
|
**Doors:** LAN `http://192.168.0.235:8000/audiobookshelf` (direct, what the phone app uses) and guarded `https://audiobooks.franksdank.cloud/audiobookshelf/`.
|
||||||
|
|
||||||
## Shape
|
## Shape
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ the local form, and the phone app offers SSO on its server-address screen. The l
|
|||||||
| The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file |
|
| The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file |
|
||||||
| Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) |
|
| Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) |
|
||||||
| Admin group members | `akadmin` (the operator's own authentik admin account — sign in as it and the claim grants site admin) and `qa-e2e-admin` (the identity the deploy gate signs in as, so the script re-adds it every run; drop it and the gate fails, not a phone) |
|
| Admin group members | `akadmin` (the operator's own authentik admin account — sign in as it and the claim grants site admin) and `qa-e2e-admin` (the identity the deploy gate signs in as, so the script re-adds it every run; drop it and the gate fails, not a phone) |
|
||||||
| The guard in front of the vhost | proxy provider `abs-proxy` (external_host `https://abs.franksdank.cloud/`, internal_host `http://caddy:8017`) + application "Audiobookshelf Guard", claimed by the embedded outpost — the ak-shell recipe in [[Caddy]] *Guard a new vhost* creates it; without that row the chain answers authentik's error page, not the login |
|
| The guard in front of the vhost | proxy provider `audiobooks-proxy` (external_host `https://audiobooks.franksdank.cloud/`, internal_host `http://caddy:8017`) + application "Audiobookshelf Guard" (its own name tracks the app, not the vhost), claimed by the embedded outpost — the ak-shell recipe in [[Caddy]] *Guard a new vhost* creates it; without that row the chain answers authentik's error page, not the login |
|
||||||
|
|
||||||
The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on,
|
The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on,
|
||||||
a first sign-in creates the account, and the claim alone decides the type. A user in
|
a first sign-in creates the account, and the claim alone decides the type. A user in
|
||||||
@@ -144,7 +144,7 @@ Both halves — authentik's provider row and ABS's settings — are asserted end
|
|||||||
`callback` against the origin it believes it is served from — `getRequestOrigin()` is
|
`callback` against the origin it believes it is served from — `getRequestOrigin()` is
|
||||||
`x-forwarded-proto://Host` (`server/utils/requestUtils.js`, read in the pinned digest
|
`x-forwarded-proto://Host` (`server/utils/requestUtils.js`, read in the pinned digest
|
||||||
2026-11-03) — so behind a TLS edge that reaches the container over plain http it computes
|
2026-11-03) — so behind a TLS edge that reaches the container over plain http it computes
|
||||||
`http://abs.franksdank.cloud` and answers its own https callback with a 400 "Invalid callback
|
`http://audiobooks.franksdank.cloud` and answers its own https callback with a 400 "Invalid callback
|
||||||
URL - must be same-origin" before the IdP is ever contacted. The Caddy `:8017` route pins
|
URL - must be same-origin" before the IdP is ever contacted. The Caddy `:8017` route pins
|
||||||
`header_up X-Forwarded-Proto https` for exactly that reason ([[Caddy]] *Guard a new vhost*);
|
`header_up X-Forwarded-Proto https` for exactly that reason ([[Caddy]] *Guard a new vhost*);
|
||||||
the LAN door needs nothing, because there browser and server agree on `http`.
|
the LAN door needs nothing, because there browser and server agree on `http`.
|
||||||
@@ -197,7 +197,7 @@ Both halves — authentik's provider row and ABS's settings — are asserted end
|
|||||||
service across a reboot ([[Quadlet Lifecycle]]).
|
service across a reboot ([[Quadlet Lifecycle]]).
|
||||||
- **Two doors, one app.** `http://192.168.0.235:8000/audiobookshelf` is the raw
|
- **Two doors, one app.** `http://192.168.0.235:8000/audiobookshelf` is the raw
|
||||||
LAN address — plain HTTP, nothing in front, what the phone app is configured
|
LAN address — plain HTTP, nothing in front, what the phone app is configured
|
||||||
with; `https://abs.franksdank.cloud/audiobookshelf/` is the guarded vhost
|
with; `https://audiobooks.franksdank.cloud/audiobookshelf/` is the guarded vhost
|
||||||
(Caddy → Anubis → the Authentik outpost → Caddy `:8017` → this container),
|
(Caddy → Anubis → the Authentik outpost → Caddy `:8017` → this container),
|
||||||
the house chain for browser traffic — [[Caddy]] *Guard a new vhost*. That
|
the house chain for browser traffic — [[Caddy]] *Guard a new vhost*. That
|
||||||
chain serves browsers only: the phone app is a non-browser client and solves
|
chain serves browsers only: the phone app is a non-browser client and solves
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Three provisioning rules, each verified live by an outage or a dead login:
|
|||||||
`_get_callback_url`) and `grant_types` is `[]`. Both kill the guard before any credential,
|
`_get_callback_url`) and `grant_types` is `[]`. Both kill the guard before any credential,
|
||||||
in this order: authentik renders its own **"Redirect URI Error"** page, and once the URIs
|
in this order: authentik renders its own **"Redirect URI Error"** page, and once the URIs
|
||||||
are filled by hand the next wall is a bodiless **HTTP 400** out of `/application/o/authorize/`
|
are filled by hand the next wall is a bodiless **HTTP 400** out of `/application/o/authorize/`
|
||||||
(both measured on `abs-proxy`, 2026-11-03). Census and repair in one idempotent pass, all
|
(both measured on the Audiobookshelf guard provider, 2026-11-03). Census and repair in one idempotent pass, all
|
||||||
providers at once: `scripts/authentik-proxy-provider-defaults.sh`; `--check` prints the
|
providers at once: `scripts/authentik-proxy-provider-defaults.sh`; `--check` prints the
|
||||||
census and exits 1 on drift, and a provider that already matches is left alone.
|
census and exits 1 on drift, and a provider that already matches is left alone.
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ authentik's error page instead of the app.
|
|||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
NAME, EXT = "abs", "https://abs.franksdank.cloud/"
|
NAME, EXT = "audiobooks", "https://audiobooks.franksdank.cloud/"
|
||||||
pp = ProxyProvider.objects.filter(name=f"{NAME}-proxy").first()
|
pp = ProxyProvider.objects.filter(name=f"{NAME}-proxy").first()
|
||||||
if pp is None:
|
if pp is None:
|
||||||
pp = ProxyProvider.objects.create(name=f"{NAME}-proxy", external_host=EXT,
|
pp = ProxyProvider.objects.create(name=f"{NAME}-proxy", external_host=EXT,
|
||||||
|
|||||||
@@ -516,7 +516,7 @@
|
|||||||
"category": "admin",
|
"category": "admin",
|
||||||
"section": "Media Library Apps",
|
"section": "Media Library Apps",
|
||||||
"description": "Audiobook server for the entropy library: its own SQLite library, its own login, per-listener positions.",
|
"description": "Audiobook server for the entropy library: its own SQLite library, its own login, per-listener positions.",
|
||||||
"url": "http://abs.franksdank.cloud/audiobookshelf/",
|
"url": "http://audiobooks.franksdank.cloud/audiobookshelf/",
|
||||||
"docs": "",
|
"docs": "",
|
||||||
"probe": "192.168.0.235:8000",
|
"probe": "192.168.0.235:8000",
|
||||||
"method": "http",
|
"method": "http",
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ const BASE = "https://www.franksdank.cloud";
|
|||||||
// Every vhost that Caddy routes through anubis:8923 (see dankclaw/Caddyfile).
|
// Every vhost that Caddy routes through anubis:8923 (see dankclaw/Caddyfile).
|
||||||
// Keep this list in lockstep with the Caddyfile: a new guarded vhost must be
|
// Keep this list in lockstep with the Caddyfile: a new guarded vhost must be
|
||||||
// added here or the guard-chain audit is incomplete.
|
// added here or the guard-chain audit is incomplete.
|
||||||
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook", "abs"];
|
const GUARDED = ["www", "search", "romm", "files", "seerr", "grafana", "prometheus", "loki", "notebook", "audiobooks"];
|
||||||
|
|
||||||
// Markers that prove app content leaked past the guard to an anonymous user. Each
|
// Markers that prove app content leaked past the guard to an anonymous user. Each
|
||||||
// marker must be a string the APP renders that the gate itself could never print —
|
// marker must be a string the APP renders that the gate itself could never print —
|
||||||
// for abs that is the Audiobookshelf login's own SSO button label (the app's OIDC
|
// for audiobooks that is the Audiobookshelf login's own SSO button label (the app's
|
||||||
// door), which authentik's gate never renders because it IS the IdP.
|
// door), which authentik's gate never renders because it IS the IdP.
|
||||||
const LEAK_MARKERS: Record<string, string[]> = {
|
const LEAK_MARKERS: Record<string, string[]> = {
|
||||||
"search.franksdank.cloud": ["SearXNG preferences"],
|
"search.franksdank.cloud": ["SearXNG preferences"],
|
||||||
@@ -29,7 +29,7 @@ const LEAK_MARKERS: Record<string, string[]> = {
|
|||||||
"grafana.franksdank.cloud": ["Dashboards"],
|
"grafana.franksdank.cloud": ["Dashboards"],
|
||||||
"prometheus.franksdank.cloud": ["Expression Browser", "Graph"],
|
"prometheus.franksdank.cloud": ["Expression Browser", "Graph"],
|
||||||
"notebook.franksdank.cloud": ["Open Notebook"],
|
"notebook.franksdank.cloud": ["Open Notebook"],
|
||||||
"abs.franksdank.cloud": ["Sign in with Authentik"],
|
"audiobooks.franksdank.cloud": ["Sign in with Authentik"],
|
||||||
};
|
};
|
||||||
|
|
||||||
test("homepage root (/) is guarded for unauthenticated users", async ({ page }) => {
|
test("homepage root (/) is guarded for unauthenticated users", async ({ page }) => {
|
||||||
|
|||||||
Reference in new issue
Block a user