dumpd /me reports the decoded discord_id; the gateway page shows it on hover
The quickest way to confirm the Vanguard profile scope mapping is live, without echoing the raw token: the browser never sees X-Authentik-Jwt, so printing it would hand the user a bearer token for the proxy provider. Decoding is display-only and never feeds an authorisation decision. Verified against eight inputs including a numeric claim, a missing claim, a non-token and malformed base64. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -414,7 +414,10 @@ Design: **Authentik authenticates once for the whole domain; Caddy authorizes pe
|
||||
`default-provider-invalidation-flow` so that also ends the Authentik login: every logout then forces a full
|
||||
Discord re-login on the next visit (and picks up group changes). Discord itself may approve silently if
|
||||
you are still logged into Discord in that browser.
|
||||
5. Debug: `https://HOST/whoami` prints the username/groups headers exactly as the apps receive them.
|
||||
5. Debug: `https://HOST/whoami` prints the username/groups headers exactly as the apps receive them, and
|
||||
`https://HOST/me` adds the `discord_id` claim decoded from `X-Authentik-Jwt` — the quickest check that the
|
||||
`Vanguard profile` scope mapping is live. The raw token is deliberately never echoed: the browser does not
|
||||
see that header today, and printing it would hand the user a bearer token they otherwise could not read.
|
||||
Group changes in Authentik apply on the next login — log out at `https://auth.HOST/` and back in.
|
||||
6. Test in a private window: `https://live.HOST/` → Discord login → portal; `https://bot.HOST/` → 403 unless
|
||||
you hold the bot role; `https://live.HOST/me/stream-key` shows your username.
|
||||
|
||||
@@ -185,6 +185,7 @@ $('l-clips').href = sub('clips') + '/'; document.querySelectorAll('a.l-clips').f
|
||||
const r = await fetch('/me', {credentials:'same-origin'}); const d = await r.json(); if (!r.ok) throw new Error(d.error || r.status);
|
||||
$('who').textContent = d.name && d.name !== d.user ? `${d.name} (${d.user})` : d.user;
|
||||
$('pills').innerHTML = d.groups.map(g => `<span class="pill">${g}</span>`).join('') || '<span class="pill">no groups yet — ask an admin for a role</span>';
|
||||
if (d.discord_id) $('who').title = 'Discord ID ' + d.discord_id; // apps identify you by this, not by your name
|
||||
const has = g => d.groups.includes(g);
|
||||
const show = (id, on) => $(id).classList.toggle('hidden', !on);
|
||||
// sections appear only for what this account can actually do (the gates check these same groups)
|
||||
|
||||
24
bin/dumpd.py
24
bin/dumpd.py
@@ -46,6 +46,27 @@ TZ = ZoneInfo(os.environ.get("DUMP_TZ", "America/New_York"))
|
||||
NIGHT_START = time(17, 0) # 5 pm
|
||||
NIGHT_END = time(2, 0) # 2 am next day
|
||||
|
||||
AUTH_JWT_HEADER = os.environ.get("AUTH_JWT_HEADER", "X-Authentik-Jwt")
|
||||
|
||||
def proxy_claim(headers, name):
|
||||
"""One claim out of the token the proxy injects, or None.
|
||||
|
||||
Authentik's proxy puts its signed token in X-Authentik-Jwt; a scope mapping on the provider adds the claims
|
||||
we care about (authentik/vanguard-profile-scope-mapping.py supplies discord_id). The payload is only read,
|
||||
never trusted beyond what the sibling headers already assert: all of them come from the same proxy, which is
|
||||
the only thing that can reach this port. Returned for display, never for an authorisation decision here.
|
||||
"""
|
||||
token = (headers.get(AUTH_JWT_HEADER) or "").strip()
|
||||
if token.count(".") != 2:
|
||||
return None
|
||||
body = token.split(".")[1]
|
||||
try:
|
||||
claims = json.loads(base64.urlsafe_b64decode(body + "=" * (-len(body) % 4)))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
value = claims.get(name)
|
||||
return str(value) if value not in (None, "") else None
|
||||
|
||||
def identity(headers):
|
||||
"""(username, is_admin) from the headers Caddy sets after Authentik; (None, False) if absent.
|
||||
dumpd binds to loopback only, so these headers can only come from Caddy."""
|
||||
@@ -440,7 +461,8 @@ class H(BaseHTTPRequestHandler):
|
||||
if not who: return self._json(401, {"error": "not authenticated"})
|
||||
groups = sorted({g.strip() for g in re.split(r"[|,]", self.headers.get(AUTH_GROUPS_HEADER) or "") if g.strip()})
|
||||
return self._json(200, {"user": who, "groups": groups, "admin": is_admin,
|
||||
"name": self.headers.get("X-Authentik-Name") or who, "email": self.headers.get("X-Authentik-Email") or ""})
|
||||
"name": self.headers.get("X-Authentik-Name") or who, "email": self.headers.get("X-Authentik-Email") or "",
|
||||
"discord_id": proxy_claim(self.headers, "discord_id")})
|
||||
if self.path.startswith("/me/clips"):
|
||||
who, _ = identity(self.headers)
|
||||
if not who: return self._json(401, {"error": "not authenticated"})
|
||||
|
||||
Reference in New Issue
Block a user