The Authentik SSO button returned "Server Error" (500): the grafana provider had
a NULL authorization_flow and the authorize view crashed building a None flow
planner. After that, the login failed with "InternalError" — no email claim
reached Grafana, so it fell back to GET <api_url>/emails, which authentik 404s.
Unit env: request authentik real scopes (openid email profile, not the GitLab
user:email that crashes the authorize view); standard userinfo API_URL with no
?api=1 (Grafana v13 turns ?api=1 into a /emails 403); email/sub/preferred_username
attribute paths; ALLOW_SIGN_UP=true since the Anubis->Authentik guard fronts the
callback. scripts/authentik-grafana-sso-fix.sh idempotently re-applies the two
IdP-side causes (NULL authorization_flow; grafana-email-scope returning a bare
string instead of {"email":..}). Verified end to end: RP login lands on / and
/api/user answers 200 with the real email, user provisioned as Viewer.
82 lines
3.9 KiB
Bash
Executable File
82 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Repair the Grafana OAuth2 relying-party SSO on Dankclaw's authentik.
|
|
#
|
|
# grafana.franksdank.cloud logs in through authentik as an OAuth2 relying party.
|
|
# Two DB-side facts (the provider was created by hand via `ak shell`, never
|
|
# repo-managed) silently broke that login, and BOTH had previously been
|
|
# mis-recorded here as a single "the property-mapping evaluator is broken, so
|
|
# userinfo returns only `sub`" myth. They are neither, and the evaluator is fine:
|
|
#
|
|
# 1. provider.authorization_flow was NULL. The authorize view builds a flow
|
|
# planner from it and crashes with
|
|
# `AttributeError: 'NoneType' object has no attribute 'slug'` — an HTTP 500
|
|
# "Server Error — Powered by authentik", before any claim is touched.
|
|
# Fix: point it at default-provider-authorization-implicit-consent, the
|
|
# flow every other provider in this instance already uses.
|
|
# 2. The `grafana-email-scope` ScopeMapping's expression was the bare string
|
|
# `user.email`. authentik merges a scope mapping's result into the token as
|
|
# a MAPPING: a returned dict adds its keys as claims; a returned string is
|
|
# keyed under the mapping's NAME ("grafana-email-scope"), so no `email`
|
|
# claim is ever emitted. Grafana, finding no email, falls back to
|
|
# GET <api_url>/emails, authentik 404s that path, and the login dies with
|
|
# "Login failed: InternalError". Fix: return
|
|
# {"email": user.email, "email_verified": True} — the shape the stock
|
|
# OpenID 'email' mapping uses. The dict-valued `grafana-profile-scope`
|
|
# mapping has always worked, which is the proof the evaluator is fine.
|
|
#
|
|
# This is authentik DB state. It is NOT in any deployable file: a blueprint
|
|
# re-import, or an ak-shell re-creation of the grafana provider, reverts BOTH
|
|
# facts and the symptom returns verbatim. Re-run this after any such rebuild.
|
|
# Every step is idempotent.
|
|
#
|
|
# Grafana's own half — SCOPES="openid email profile", a standard userinfo API_URL
|
|
# with no `?api=1`, and ALLOW_SIGN_UP=true — lives in
|
|
# dankclaw/observability/quadlet/grafana.container and deploys normally.
|
|
#
|
|
# Usage: authentik-grafana-sso-fix.sh (override host: AUTHENTIK_HOST=…)
|
|
set -euo pipefail
|
|
HOST="${AUTHENTIK_HOST:-dankclaw}"
|
|
|
|
ssh "$HOST" 'podman exec -i authentik-server ak shell' <<'PYEOF'
|
|
from authentik.flows.models import Flow
|
|
from authentik.providers.oauth2.models import OAuth2Provider
|
|
|
|
p = OAuth2Provider.objects.filter(name="grafana").first()
|
|
if not p:
|
|
raise SystemExit("no OAuth2Provider named 'grafana'")
|
|
|
|
# 1. The authorization flow every other provider uses.
|
|
want = Flow.objects.filter(slug="default-provider-authorization-implicit-consent").first()
|
|
if not want:
|
|
raise SystemExit("default-provider-authorization-implicit-consent flow is missing")
|
|
if p.authorization_flow_id != want.pk:
|
|
p.authorization_flow = want
|
|
p.save()
|
|
print(f"set grafana.authorization_flow -> {want.slug}")
|
|
else:
|
|
print(f"grafana.authorization_flow already {want.slug}")
|
|
|
|
# 2. The email scope mapping must RETURN A DICT, not a bare string.
|
|
m = p.property_mappings.filter(name="grafana-email-scope").first()
|
|
if not m:
|
|
raise SystemExit("grafana-email-scope mapping is missing")
|
|
fixed = 'return {"email": user.email, "email_verified": True}'
|
|
if m.expression.strip() != fixed:
|
|
m.expression = fixed
|
|
m.save()
|
|
print("repaired grafana-email-scope -> returns {email, email_verified}")
|
|
else:
|
|
print("grafana-email-scope expression already correct")
|
|
|
|
# 3. The id_token must carry the scope claims; Grafana reads identity from it.
|
|
if not p.include_claims_in_id_token:
|
|
p.include_claims_in_id_token = True
|
|
p.save()
|
|
print("enabled include_claims_in_id_token")
|
|
else:
|
|
print("include_claims_in_id_token already on")
|
|
PYEOF
|
|
|
|
echo "Done. Verify: sign in at https://grafana.franksdank.cloud via Authentik;"
|
|
echo "the round trip must land on / and /api/user must answer 200 with an email."
|