fix(identity): stop an idle hour from breaking gated pages - #418
Conversation
Leaving a gated tab alone for an hour and coming back showed a page with its icons missing until a second reload. It happened on the MTG lobby, and it would happen on the landing page and the music library the same way. The gates run on authentik's default access token of one hour, which 50-proxy-gates.yaml never pinned. That is a regression against the Authelia sessions they replaced, which lasted a week idle. Once the token is gone the outpost does not refresh in the background: the next request gets a full OAuth round trip, and the browser asks for the page and every icon at the same moment, so each unauthenticated one starts its own round trip and the callbacks overwrite each other's state cookie. Only the last can match. The rest fail with "oauth state does not match the session" and render broken. The reload works because by then a session exists. Nobody is prompted for a password in any of it, which is why it reads as breakage and not as a login. All three gates now pin a one-day session. Nobody meets a day over lunch or a meeting, and a revoked account still loses these gates within a day, because the outpost answers from its own session store instead of asking authentik per request. The MTG lobby's icons and brand marks are also served ahead of the gate, so they cannot join the race on the loads that do still expire. They are branding, not secrets, which is the same reasoning that already exempts the apex brand assets for mail clients. It is a handle block rather than a matcher on the gate itself: an exempt path that does not exist has to 404 there, where falling through to the SPA fallback would serve index.html to anyone asking for /icons/anything.png without a session. Both changes are pinned by tests, including the ordering that makes them work, and the migration doc records this as evidence item 4 - answered by observation, so the compressed-clock experiment it describes is no longer needed to unblock 6f.
d98db14 to
21d05d4
Compare
|
Deployed and verified 2026-08-21. Proven from an anonymous client:
That 404 is the check worth keeping: it is the failure a Read the timing before reading any graph. Sessions that already existed at deploy time keep their original one-hour expiry (upstream #12751), so the behaviour this PR fixes can still occur for about a day. A quiet evening is not the fix working. With #417's Caddy logging now live, the honest before/after is One interaction with #417 worth recording: the exempted assets terminate before the gate, so they are 200s carrying no identity headers and the Alloy stage drops them. Their absence from Loki is the exemption working, not a gap in the logging. |
…00 (#421) mon-1 made the three gated hosts log every request and had Alloy drop the 2xx and 304 on the way to Loki, so what reaches Loki is only the refusals. That is right everywhere except one path. When a gate session expires mid-page-load the browser re-requests the page and every asset at once, each starts its own OAuth round trip, and the parallel callbacks overwrite each other's state cookie. Caddy answers 200 to the failures too, so the drop discarded exactly the requests that broke, and the only record left was the outpost's own "oauth state does not match the session". Diagnosing #418 had to be done from that log because the access log could not help. /outpost.goauthentik.io/ is now exempt from the status drop, whatever it returns. It is the auth handshake itself, it is only touched on a login round trip, so it is not the firehose the drop exists to avoid, and it is the one place where a 200 is not evidence that anything worked. The drop is not widened anywhere else. The exemption is a line filter on the selector, because Alloy matches with Go RE2 and RE2 has no negative lookahead: "drop a 2xx unless the URI is an outpost path" cannot be written as one expression. It is the regex form anchored on the "uri" key rather than a plain substring test, because Caddy logs the request headers too, so a bare substring filter would also exempt any ordinary success whose Referer pointed at a handshake page. The client-cancelled-stream drop moves to a block of its own. It carries no URI, so the exemption is meaningless to it and sharing a filtered selector would only make it look conditional on something it cannot depend on. tests/test-alloy-log-drops.sh pins both directions: a failed callback that must now be KEPT, and an asset whose Referer merely quotes the path, which must still go. Its parser had to learn about line filters first, or it would have reported a drop for a line the running pipeline keeps.
What broke
Leave a gated tab alone for an hour, come back, and the page loads with its
icons missing until you reload a second time. Seen on the MTG lobby today; the
landing page and the music library are set up the same way.
Why
The three gates run on authentik's default access token of one hour, which
50-proxy-gates.yamlnever pinned. That is a regression against the Autheliasessions they replaced, which lasted a week idle.
Once the token is gone the outpost does not refresh in the background. The next
request gets a full OAuth round trip, and the browser asks for the page and
every icon at the same moment, so each unauthenticated one starts its own
round trip and the callbacks overwrite each other's single state cookie. Only
the last one can match. The rest fail with
oauth state does not match the sessionand render as broken assets. The reload works because by then asession exists.
Evidence: ten of those warnings at 13:49:16, one per asset, naming exactly the
icons that went missing (
/icons/sections/play.png,decks.png,draft.png,online.png,settings.png,apple-touch-icon.png,/favicon-16x16.png,/logo.webp,/logo_only.webp,/wordmark.webp).Nobody is prompted for a password anywhere in this, which is why it reads as
breakage rather than as a login, and it is invisible to
curl, which asks forone thing at a time.
What this changes
access_token_validity: hours=24. Nobody meets a dayover lunch or a meeting, and a revoked account still loses these gates within
a day, because the outpost answers from its own session store instead of
asking authentik per request. Raising it further weakens that.
they cannot join the race on the loads that do still expire. Branding, not
secrets: the same reasoning that already exempts the apex brand assets for
mail clients. Written as a
handleblock rather than anot pathmatcher onthe gate, because an exempt path that does not exist has to 404 there, where
falling through to the SPA fallback would serve
index.htmlto anyone askingfor
/icons/anything.pngwith no session.handleafter the gate exempts nothing).docs/authentik-migration.mdrecords this as 6f evidence item 4, answered byobservation. The compressed-clock experiment it describes is no longer needed
to unblock 6f, and its "the blueprint does not pin this field" note is
reversed, because now it does.
Checks
tests/run.sh: 0 failing tests.the pinned
caddy:2.11.4-alpineimage:Valid configuration.pre-commit run --all-files: all hooks pass.Not deployed. Note for whoever deploys: existing sessions keep their old
one-hour expiry (upstream #12751), so the first day after this applies still
behaves like the old value.
Conflicts
Touches
web/caddy/Caddyfile.j2anddocs/authentik-migration.md, whichPR #417 (mon-1, Caddy access logging) also touches. Whichever merges second
needs a rebase; the changes are in different parts of both files.