fix(authentik): census and repair the OAuth defaults on every proxy provider

A proxy provider built by hand in `ak shell` never runs
ProxyProvider.set_oauth_defaults() — the REST serializer does, the ORM does not —
so the row looks fully configured while redirect_uris is empty (they are derived
from external_host by _get_callback_url()), grant_types is [], and
authorization_flow is NULL. Each one kills a guarded login in a different,
unrelated-looking way, all before any credential is requested: authentik's own
"Redirect URI Error" page, then a bodiless HTTP 400 from /application/o/authorize/
once the URIs are filled by hand, and a 500 NoneType.slug in the flow planner.
abs-proxy (abs.franksdank.cloud) hit the first two on 2026-11-03.

The script calls the vendor's own set_oauth_defaults() and points the
authorization flow at default-provider-authorization-implicit-consent for every
proxy provider, idempotently; a provider that already matches is untouched, so a
deliberate deviation is never flattened. `--check` is the census (exit 1 on
drift, writes nothing) for gate use. Applied live 2026-11-03: abs-proxy fixed,
the other ten (REST-created) already compliant, census now reports drift: 0.

This is authentik DB state and no deployable file carries it — a blueprint
re-import or an ak-shell re-creation reverts it and the symptom returns, so the
law and the failure signatures are recorded on the outpost page as provisioning
rule 3.
This commit is contained in:
Frank committed 2026-11-03 01:16:35 -08:00
1 parent 17ea2f4110
commit fadbe10420
2 files changed
+106 -1

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Census and repair the OAuth2 defaults on every authentik PROXY provider.
#
# A proxy provider built by hand in `ak shell` never runs
# ProxyProvider.set_oauth_defaults() — the REST serializer calls it, the ORM does
# not — so the row looks perfectly configured (external_host, internal_host,
# application bound, embedded-outpost claim present) while three OAuth facts are
# blank. Each one kills the guarded login in a different, unrelated-looking way,
# all BEFORE any credential is asked for:
#
# * redirect_uris empty -> authentik answers its own page:
# "Redirect URI Error — The request fails due to a missing, invalid, or
# mismatching redirection URI (redirect_uri)". Measured on abs-proxy
# (abs.franksdank.cloud) 2026-11-03.
# * grant_types empty -> the authorize endpoint refuses the outpost's
# authorization-code request with a bodiless HTTP 400 (Chromium renders
# "This page isn't working / HTTP ERROR 400"). Same provider, one step
# later, after the URIs were filled in by hand.
# * authorization_flow NULL -> the authorize view crashes building its flow
# planner: AttributeError 'NoneType' object has no attribute 'slug', HTTP
# 500. That is what authentik-grafana-sso-fix.sh exists for.
#
# So: a guard that shows ANY of those three pages is not an Anubis problem and
# not a Caddy problem — run this script before touching the route.
#
# Repair calls the VENDOR's own set_oauth_defaults() (grant_types = authz code +
# client credentials + password, confidential client, the stock scope mappings,
# redirect_uris derived from external_host by _get_callback_url) and points the
# authorization flow at default-provider-authorization-implicit-consent, the flow
# every working provider here uses. Providers that already match are untouched,
# so a deliberate per-provider deviation is never silently flattened.
#
# This is authentik DB state. It is NOT in any deployable file: a blueprint
# re-import, or an ak-shell re-creation of a provider, reverts these facts and
# the symptom returns verbatim. Re-run after any such rebuild. Idempotent.
#
# Usage: authentik-proxy-provider-defaults.sh [--check]
# no argument repair what drifts, print one line per provider
# --check census only, exit 1 if anything drifts (gate use)
# override host: AUTHENTIK_HOST=…
set -euo pipefail
HOST="${AUTHENTIK_HOST:-dankclaw}"
MODE="${1:-}"
case "$MODE" in
"" | --check) : ;;
*) echo "usage: $(basename "$0") [--check]" >&2; exit 2 ;;
esac
ssh "$HOST" 'podman exec -i authentik-server ak shell' <<PYEOF
from authentik.flows.models import Flow
from authentik.providers.proxy.models import ProxyProvider, _get_callback_url
CHECK = $( [ "$MODE" = "--check" ] && echo True || echo False )
FLOW = "default-provider-authorization-implicit-consent"
WANT_GRANTS = ["authorization_code", "client_credentials", "password"]
flow = Flow.objects.filter(slug=FLOW).first()
if not flow and not CHECK:
raise SystemExit("flow %s is missing; nothing was changed" % FLOW)
drift = 0
for p in ProxyProvider.objects.all().order_by("pk"):
want_uris = [u.url for u in _get_callback_url(p.external_host)]
have_uris = [u.url for u in p.redirect_uris]
bad = []
if [str(g) for g in p.grant_types] != WANT_GRANTS:
bad.append("grant_types=%s" % [str(g) for g in p.grant_types])
if have_uris != want_uris:
bad.append("redirect_uris=%s" % have_uris)
if p.authorization_flow_id is None:
bad.append("authorization_flow=None")
if not bad:
print("ok %s (ext %s)" % (p.name, p.external_host))
continue
drift += 1
if CHECK:
print("DRIFT %s: %s" % (p.name, "; ".join(bad)))
continue
print("fix %s: %s" % (p.name, "; ".join(bad)))
p.set_oauth_defaults()
if p.authorization_flow_id is None:
p.authorization_flow = flow
p.save()
after = ProxyProvider.objects.get(pk=p.pk)
print(" -> grants %s, %d redirect URIs, flow %s"
% ([str(g) for g in after.grant_types], len(after.redirect_uris),
after.authorization_flow and after.authorization_flow.slug))
print("providers checked, drift: %d" % drift)
raise SystemExit(1 if (CHECK and drift) else 0)
PYEOF
echo "Verify: walk the guarded vhost in a browser — Anubis solves, authentik"
echo "renders its login form (no Redirect URI Error, no HTTP 400), and the"
echo "callback lands back on the app."