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:
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