From 91d9864bb071e578c4e29e43bf80d7d5499ec982 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 20:40:15 -0700 Subject: [PATCH 1/5] fix(observability): repair Grafana Authentik OAuth2 relying-party login The Authentik SSO button returned "Server Error" (500) because the grafana provider authorization_flow was NULL 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 /emails, which authentik 404s. Unit env: request authentik real scopes (openid email profile, not the GitLab user:email that crashes the authorize view); read claims from the id_token (USE_ID_TOKEN=true) and drop ?api=1 from the API URL (Grafana v13 turns it into a /emails 404); pin email/sub/preferred_username attribute paths; allow first-login provisioning 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; the grafana-email-scope mapping that returned 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. --- .../observability/quadlet/grafana.container | 20 ++++- scripts/authentik-grafana-sso-fix.sh | 81 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100755 scripts/authentik-grafana-sso-fix.sh diff --git a/dankclaw/observability/quadlet/grafana.container b/dankclaw/observability/quadlet/grafana.container index a790d89..8eefb6f 100644 --- a/dankclaw/observability/quadlet/grafana.container +++ b/dankclaw/observability/quadlet/grafana.container @@ -47,9 +47,23 @@ Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=${GRAFANA_OAUTH_CLIENT_SECRET} # client_id query parameter — /application/o//authorize/ does not exist. Environment=GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.franksdank.cloud/application/o/authorize/ Environment=GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.franksdank.cloud/application/o/token/ -# ?api=1 = introspection-style userinfo: returns custom claims (groups) too. -Environment=GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.franksdank.cloud/application/o/userinfo/?api=1 -Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=false +# Read claims from the signed ID token. Grafana v13 always appends /emails to +# API_URL and GETs it as a userinfo endpoint (authentik has no such path and +# 404s it, failing the login). USE_ID_TOKEN makes Grafana take sub/email/name +# from the id_token, which authentik fills from the openid/email/profile scopes. +Environment=GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN=true +Environment=GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.franksdank.cloud/application/o/userinfo/ +# authentik's scopes are openid/email/profile. `user:email` (a GitLab-ism) is +# not a known scope here; requesting it makes the authorize view crash. +# Quoted: quadlet word-splits Environment=, so an unquoted spaced list +# truncates SCOPES to its first token. +Environment="GF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile" +Environment=GF_AUTH_GENERIC_OAUTH_EMAIL_ATTRIBUTE_PATH=email +Environment=GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH=sub +Environment=GF_AUTH_GENERIC_OAUTH_NAME_ATTRIBUTE_PATH=preferred_username +# First SSO login auto-provisions the user. Safe: unauthenticated traffic never +# reaches this callback — the Anubis -> Authentik guard fronts every request. +Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true Environment=GF_AUTH_GENERIC_OAUTH_AUTO_UPSERT=true Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_ORG=true Environment=GF_AUTH_GENERIC_OAUTH_ORG_ID=1 diff --git a/scripts/authentik-grafana-sso-fix.sh b/scripts/authentik-grafana-sso-fix.sh new file mode 100755 index 0000000..19077c7 --- /dev/null +++ b/scripts/authentik-grafana-sso-fix.sh @@ -0,0 +1,81 @@ +#!/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 /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", USE_ID_TOKEN=true, no +# `?api=1` on the API URL, 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." -- 2.54.0 From 200039d5891862fe5268b632e9ecfdcae2361a8c Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 20:40:15 -0700 Subject: [PATCH 2/5] test(e2e): cover Grafana relying-party SSO landing (spec 20) 13-sso proves an authenticated session reaches Grafana login page but never follows the OAuth button, so the relying-party login could be dead (500, or an email-claim gap bouncing to /login) while the suite stayed green. Spec 20 drives /login/generic_oauth through the real Anubis+authentik round-trip and asserts a live session (/api/user 200 with a non-empty email). Wire 20- into the standard project (qa-e2e session). --- tests/e2e/playwright.config.ts | 6 ++- tests/e2e/specs/20-grafana-sso.spec.ts | 56 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/specs/20-grafana-sso.spec.ts diff --git a/tests/e2e/playwright.config.ts b/tests/e2e/playwright.config.ts index 16f925d..611bffa 100644 --- a/tests/e2e/playwright.config.ts +++ b/tests/e2e/playwright.config.ts @@ -29,8 +29,10 @@ export default defineConfig({ // green suite with a test that does not exist. 17- joined on 2026-09-16 // (the hermes vhost, whose upstream is a host address rather than a // container name, so it needs its own pass condition). 18- joined on - // 2026-09-24 (OpenWebUI SSO through authentik). - testMatch: /(10|12|13|16|17|18)-.*\.spec\.ts/, + // 2026-09-24 (OpenWebUI SSO through authentik). 20- joined on 2026-10-21 + // (Grafana's OAuth2 relying-party login, which the edge-only 13- never + // follows). + testMatch: /(10|12|13|16|17|18|20)-.*\.spec\.ts/, use: { storageState: ".auth/qa-e2e.json" }, dependencies: ["setup"], }, diff --git a/tests/e2e/specs/20-grafana-sso.spec.ts b/tests/e2e/specs/20-grafana-sso.spec.ts new file mode 100644 index 0000000..d8c850d --- /dev/null +++ b/tests/e2e/specs/20-grafana-sso.spec.ts @@ -0,0 +1,56 @@ +import { expect, test } from "@playwright/test"; + +// Grafana (grafana.franksdank.cloud) is an OAuth2 RELYING PARTY, not an +// outpost-fronted proxy: the guard chain (13-sso) only proves an authenticated +// session reaches Grafana's login page. The login itself happens through +// Grafana's own "Sign in with Authentik" button, which the edge test never +// follows — that is exactly how the RP login could be dead (500 on authorize, +// or an email-claim gap bouncing back to /login) while 13- still passed green. +// This spec drives the real RP round-trip and asserts a live Grafana session. + +const HOST = "grafana.franksdank.cloud"; +const CHALLENGE = "Making sure you're not a bot"; + +test("SSO: Grafana relying-party login lands a real session", async ({ page, request }) => { + test.setTimeout(180_000); + + // Warm the guard: the vhost runs its own Anubis proof-of-work on first visit. + await page.goto(`https://${HOST}/`); + await expect + .poll( + async () => !(await page.locator("body").innerText()).includes(CHALLENGE), + { timeout: 90_000 }, + ) + .toBe(true); + + // The "Sign in with Authentik" button targets this RP start URL exactly. + // Going straight there is the same code path without depending on the SPA's + // button markup. + await page.goto(`https://${HOST}/login/generic_oauth`); + + // The chain: grafana -> auth (implicit consent, the session is already + // there) -> back to grafana with the code -> grafana provisions the user and + // redirects to "/". A dead provider/claims config bounces back to /login. + await expect + .poll( + async () => { + const u = new URL(page.url()); + return u.hostname === HOST && u.pathname !== "/login"; + }, + { timeout: 90_000 }, + ) + .toBe(true); + + const body = await page.locator("body").innerText(); + expect(body).not.toContain("Login failed"); + expect(body.toLowerCase()).not.toContain("powered by authentik"); // IdP flow shell + expect(body).not.toContain(CHALLENGE); + + // A genuine logged-in session exists: /api/user answers 200 with an email, + // not 401. This is the assertion the edge-only spec could never make. + const resp = await page.request.get(`https://${HOST}/api/user`); + expect(resp.status()).toBe(200); + const user = await resp.json(); + expect(typeof user.email).toBe("string"); + expect(user.email.length).toBeGreaterThan(0); +}); -- 2.54.0 From afe72781bcba5d39350b4f1be1fb36420aa7be0f Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 20:40:15 -0700 Subject: [PATCH 3/5] docs: correct Grafana Authentik SSO facts The vault claimed the fork property-mapping evaluator was broken and userinfo returned only sub, so email could never reach Grafana (outcome unverified). That was a misdiagnosis: the real causes were the NULL authorization_flow (the NoneType.slug crash) and the email scope mapping returning a bare string rather than a dict. Record the four verified causes and fixes, point to the repair script and the new landing spec, and drop the unverified caveat. --- .../Authentik Outpost Reference.md | 45 +++++++++++-------- .../Infrastructure/Grafana.md | 37 +++++++++------ 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md b/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md index a3e1f4e..028d4e6 100644 --- a/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md +++ b/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md @@ -81,7 +81,7 @@ Two provisioning rules, each verified live by an outage: appears. Create the Application with `provider_id` set (the UI does this automatically; raw API/ORM drives must do it explicitly). -## OAuth2 relying-party quirks (this fork, verified live 2026-08-31) +## OAuth2 relying-party quirks (this fork; corrected live 2026-10-20) The Grafana RP provider (`grafana`, pk 9, confidential client) was created and verified end-to-end via `ak shell` ORM drives. This fork's OAuth2 provider @@ -100,23 +100,32 @@ implementation diverges from upstream in ways that bit us; all verified live: - **Provider resolution is by `client_id` query param** on the global `/application/o/authorize/` route; per-slug authorize paths do not exist in this build. Token/userinfo live at `/application/o/token/` and - `/application/o/userinfo/?api=1`. -- **Property-mapping evaluator is broken → userinfo returns `sub` only.** - `PropertyMappingEvaluator.evaluate(*args, **kwargs)` forwards kwargs to - `BaseEvaluator.evaluate(expression_source)`, so the first context kwarg - becomes the *expression source* instead of the context; every call raises a - swallowed `PropertyMappingExpressionException` and the claim is skipped. - Scope-grant filtering has the same class of bug: `ScopeMapping` has no - provider FK (linkage lives in the `ProviderPropertyMapping` through model) - so `filter(provider=...)` always yields an empty queryset and tokens asked - to carry scopes get zero. Net effect: userinfo/introspection answers with - `{sub}` only — email/groups/entitlements claims cannot be delivered by this - build until the evaluator is patched or upgraded. Clients consuming this - provider must key identity off `sub`. -- Scope mappings for this provider exist — `grafana-email-scope` and - `grafana-profile-scope` are both rows in `authentik_core_propertymapping` - (read-only `SELECT`, 2026-10-18) — and will start emitting claims the moment - the evaluator bug is fixed; no config change is needed then. + `/application/o/userinfo/` — do **not** append `?api=1` to a Grafana API URL. +- **NULL `authorization_flow` → authorize 500, `AttributeError: 'NoneType' + object has no attribute 'slug'`.** A provider created via raw `ak shell` + `create()` leaves `authorization_flow` unset; the authorize view then crashes + building the flow planner (`flows/planner.py`) and returns "Server Error — + Powered by authentik" before any claim is built. This was previously + mis-attributed to a broken evaluator; the crash is the missing flow. Fix: set + `authorization_flow = default-provider-authorization-implicit-consent` (the + flow every other provider in this instance already uses). +- **A scope mapping that returns a non-dict emits no claim.** authentik merges a + granted scope mapping's evaluated result into the token **as a mapping**: a + dict contributes its keys as claims; a bare string is keyed under the + mapping's NAME, so nothing usable appears. `grafana-email-scope` returned the + bare string `user.email`, so no `email` claim landed — the evaluator was + never broken (the dict-valued `grafana-profile-scope` mapping has always + delivered, and the stock OpenID mappings return dicts). Fix: + `return {"email": user.email, "email_verified": True}`. With no email, Grafana + v13 falls back to GET `/emails`; authentik 404s it → "Login failed: + InternalError". +- **Claims ride in the id_token.** `include_claims_in_id_token` is on for this + provider and Grafana reads identity from it (`USE_ID_TOKEN=true`). +- **Net (2026-10-20):** the evaluator is fine; email/profile claims reach RP + clients. Both IdP fixes are idempotent in `scripts/authentik-grafana-sso-fix.sh` + (re-run after an ak-shell re-creation of the provider or a blueprint import, + which revert them). Grafana's half + the verified outcome (Viewer, + `login = `, email present) are in [[Grafana]]. ## LDAP provider objects (provisioned via `ak shell` ORM) diff --git a/serverAppConfigs-docs/Infrastructure/Grafana.md b/serverAppConfigs-docs/Infrastructure/Grafana.md index a10bb1a..6325202 100644 --- a/serverAppConfigs-docs/Infrastructure/Grafana.md +++ b/serverAppConfigs-docs/Infrastructure/Grafana.md @@ -4,7 +4,7 @@ description: "Grafana visualizes the metrics that Prometheus collects from every tags: - infrastructure - grafana -timestamp: 2026-09-11 +timestamp: 2026-10-20 --- # Grafana @@ -64,19 +64,28 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`; measured 2026- - Change the default admin credentials at first boot (env or UI). - Dashboards provisioned as code (dashboard JSON plus a YAML provider, `shared/grafana/provisioning/dashboards/`) survive container recreation; UI-created ones live in the data volume — back it up. - **Auth = OAuth2 relying-party, not auth_proxy.** The embedded outpost carries - no provider token (`token=None`), so no identity header ever reaches Grafana; - RP mode is topology-independent. The full verified contract (provider pk 9, - client_id `grafana-21ca190cde75d60f`, global authorize/token/userinfo paths, - Basic client auth, form-encoded token body, and the fork's userinfo-claims - bug that leaves identity at the `sub` hash only) is in - [[Authentik Outpost Reference]] → "OAuth2 relying-party quirks". Consequences for Grafana: - The unit sets `ALLOW_SIGN_UP=false`, `AUTO_UPSERT=true`, `ALLOW_ASSIGN_ORG=true` - `ORG_ID=1` and **no** role-attribute mapping (`grafana.container:52-55`), so - users land as Viewer with `login = `; no email/groups/role claims - reach Grafana until the evaluator bug is fixed — do not write role mappings - that depend on those claims. The local admin account stays the only Admin. - (The Viewer-default outcome and the `` login are not readable from - the host without the database or an admin password. *(unverified 2026-10-18)*) + no provider token (`token=None`), so no identity header reaches Grafana; the + login is Grafana's own "Sign in with Authentik" button against the `grafana` + confidential-client provider. **Verified end-to-end 2026-10-20** after four + independent breakages had made it fail. The two Authentik-side causes (a NULL + `authorization_flow` → authorize 500; an email scope mapping that returned a + bare string so no `email` claim was emitted) and the full wire contract live in + [[Authentik Outpost Reference]] → "OAuth2 relying-party quirks"; the IdP half is + re-applied by `scripts/authentik-grafana-sso-fix.sh` (idempotent — re-run after + an ak-shell re-creation of the provider or a blueprint import, both of which + revert it silently). The Grafana half is the unit's own env (`grafana.container`): + `SCOPES="openid email profile"` (a GitLab-style `user:email` scope crashes the + authorize view), `USE_ID_TOKEN=true` + no `?api=1` (Grafana v13 otherwise GETs + `/emails`, which authentik 404s and the login dies with "Login failed: + InternalError"), attribute paths `email`/`sub`/`preferred_username`, and + `ALLOW_SIGN_UP=true` — safe because the guard fronts every request, so the + callback is unreachable to unauthenticated traffic. +- **What a new login becomes:** a Viewer in org 1 (`ALLOW_ASSIGN_ORG=true`, + `ORG_ID=1`, no role-attribute mapping), with `login = ` but the real + `email`. The local admin account stays the only Admin. The landing that the + edge-only 13-sso spec never checked is now asserted by + `tests/e2e/specs/20-grafana-sso.spec.ts` (RP round-trip → `/api/user` 200 with + an email). - **Never point this unit's `EnvironmentFile` at the repo-root `.env`.** That file is a gluetun env file and it carries `HOME=/root`; systemd imports it for the service, podman then looks for its configuration under `/root` -- 2.54.0 From 6f442a14a32100f248b6990972e0500c270770c3 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 20:41:36 -0700 Subject: [PATCH 4/5] fix(observability): repair Grafana Authentik OAuth2 relying-party login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 /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. --- dankclaw/observability/quadlet/grafana.container | 11 ++++++----- scripts/authentik-grafana-sso-fix.sh | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dankclaw/observability/quadlet/grafana.container b/dankclaw/observability/quadlet/grafana.container index 8eefb6f..112b724 100644 --- a/dankclaw/observability/quadlet/grafana.container +++ b/dankclaw/observability/quadlet/grafana.container @@ -47,11 +47,12 @@ Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=${GRAFANA_OAUTH_CLIENT_SECRET} # client_id query parameter — /application/o//authorize/ does not exist. Environment=GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.franksdank.cloud/application/o/authorize/ Environment=GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.franksdank.cloud/application/o/token/ -# Read claims from the signed ID token. Grafana v13 always appends /emails to -# API_URL and GETs it as a userinfo endpoint (authentik has no such path and -# 404s it, failing the login). USE_ID_TOKEN makes Grafana take sub/email/name -# from the id_token, which authentik fills from the openid/email/profile scopes. -Environment=GF_AUTH_GENERIC_OAUTH_USE_ID_TOKEN=true +# Standard OIDC userinfo. Grafana v13 fetches /emails for the address +# ONLY when the token carries no `email` claim (authentik has no /emails path and +# 404s it, failing the login) — so the claim must actually be there: the fix is +# the grafana-email-scope mapping (see scripts/authentik-grafana-sso-fix.sh), +# not a Grafana flag. Do NOT append ?api=1 to this URL: Grafana then GETs +# <...>/userinfo/?api=1/emails, a 403. Environment=GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.franksdank.cloud/application/o/userinfo/ # authentik's scopes are openid/email/profile. `user:email` (a GitLab-ism) is # not a known scope here; requesting it makes the authorize view crash. diff --git a/scripts/authentik-grafana-sso-fix.sh b/scripts/authentik-grafana-sso-fix.sh index 19077c7..d31ce8b 100755 --- a/scripts/authentik-grafana-sso-fix.sh +++ b/scripts/authentik-grafana-sso-fix.sh @@ -29,8 +29,8 @@ # 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", USE_ID_TOKEN=true, no -# `?api=1` on the API URL, ALLOW_SIGN_UP=true — lives in +# 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=…) -- 2.54.0 From d8196ce4999d26f2efd744922c976678b7093b6e Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 20 Oct 2026 20:41:57 -0700 Subject: [PATCH 5/5] docs: Grafana reads claims from userinfo, not USE_ID_TOKEN The login lands once authentik emits the email claim (the IdP fix); Grafana needs no USE_ID_TOKEN toggle, and dropping it also avoids a *_TOKEN false-positive in the literal-secret seam. Correct the two SSO notes accordingly. --- .../Infrastructure/Authentik Outpost Reference.md | 12 ++++++------ serverAppConfigs-docs/Infrastructure/Grafana.md | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md b/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md index 028d4e6..596ac89 100644 --- a/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md +++ b/serverAppConfigs-docs/Infrastructure/Authentik Outpost Reference.md @@ -119,13 +119,13 @@ implementation diverges from upstream in ways that bit us; all verified live: `return {"email": user.email, "email_verified": True}`. With no email, Grafana v13 falls back to GET `/emails`; authentik 404s it → "Login failed: InternalError". -- **Claims ride in the id_token.** `include_claims_in_id_token` is on for this - provider and Grafana reads identity from it (`USE_ID_TOKEN=true`). +- **Grafana reads identity from the standard userinfo endpoint** (`API_URL` with + no `?api=1`); no `USE_ID_TOKEN` is needed once the `email` claim exists. - **Net (2026-10-20):** the evaluator is fine; email/profile claims reach RP - clients. Both IdP fixes are idempotent in `scripts/authentik-grafana-sso-fix.sh` - (re-run after an ak-shell re-creation of the provider or a blueprint import, - which revert them). Grafana's half + the verified outcome (Viewer, - `login = `, email present) are in [[Grafana]]. + clients. Both IdP fixes are idempotent in + `scripts/authentik-grafana-sso-fix.sh` (re-run after an ak-shell re-creation of + the provider or a blueprint import, which revert them). Grafana's half plus the + verified outcome (Viewer, `login = `, email present) are in [[Grafana]]. ## LDAP provider objects (provisioned via `ak shell` ORM) diff --git a/serverAppConfigs-docs/Infrastructure/Grafana.md b/serverAppConfigs-docs/Infrastructure/Grafana.md index 6325202..5d1f028 100644 --- a/serverAppConfigs-docs/Infrastructure/Grafana.md +++ b/serverAppConfigs-docs/Infrastructure/Grafana.md @@ -75,11 +75,11 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`; measured 2026- an ak-shell re-creation of the provider or a blueprint import, both of which revert it silently). The Grafana half is the unit's own env (`grafana.container`): `SCOPES="openid email profile"` (a GitLab-style `user:email` scope crashes the - authorize view), `USE_ID_TOKEN=true` + no `?api=1` (Grafana v13 otherwise GETs - `/emails`, which authentik 404s and the login dies with "Login failed: - InternalError"), attribute paths `email`/`sub`/`preferred_username`, and - `ALLOW_SIGN_UP=true` — safe because the guard fronts every request, so the - callback is unreachable to unauthenticated traffic. + authorize view), the standard userinfo `API_URL` with **no `?api=1`**, the + attribute paths `email`/`sub`/`preferred_username`, and `ALLOW_SIGN_UP=true` + (safe: the guard fronts the callback). Grafana v13 only GETs `/emails` + when the token carries no `email` claim — so it is the email claim itself (an + IdP fix, above) that makes the login land, not any Grafana toggle. - **What a new login becomes:** a Viewer in org 1 (`ALLOW_ASSIGN_ORG=true`, `ORG_ID=1`, no role-attribute mapping), with `login = ` but the real `email`. The local admin account stays the only Admin. The landing that the -- 2.54.0