From 9d678122d78dd833c005787c0b38caa84f7f95f3 Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 31 Aug 2026 06:06:57 -0700 Subject: [PATCH] docs: grafana OAuth2-RP contract + authentik fork quirks (live-verified 2026-08-31) Token exchange chain verified end-to-end in-process on the live provider (pk 9): authorize -> ORM-minted code -> form-encoded token exchange with Basic client auth -> userinfo. Key findings documented: - Application row + grant_types must exist before authorize can complete - token endpoint consumes form-encoded bodies only (JSON body => no grant_type parsed -> unsupported_grant_type); client auth is Basic-only - PropertyMappingEvaluator.evaluate(*args,**kwargs) forwards kwargs to BaseEvaluator.evaluate(expression_source), so context kwargs become the expression source and every evaluation raises a swallowed exception; userinfo therefore answers sub-only claims for every provider in this fork. Scope granting filter (provider= M2M descriptor) is always empty. - Grafana identity = sub hash until the evaluator bug is fixed upstream. --- .../Infrastructure/Authentik.md | 36 +++++++++++++++++++ .../Infrastructure/Grafana.md | 13 ++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/serverAppConfigs-docs/Infrastructure/Authentik.md b/serverAppConfigs-docs/Infrastructure/Authentik.md index 3028740..f3694f1 100644 --- a/serverAppConfigs-docs/Infrastructure/Authentik.md +++ b/serverAppConfigs-docs/Infrastructure/Authentik.md @@ -119,6 +119,42 @@ three flows (pk1's values: `default-authentication-flow`, 3-second cycle. +## OAuth2 relying-party quirks (this fork, verified live 2026-08-31) + +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 +implementation diverges from upstream in ways that bit us; all verified live: + +- **Application row required.** `OAuth2Provider.objects.create()` does not + create the single-table-inheritance `Application` row; without it the flow + executor can never complete an authorize request. Create both rows. +- **`grant_types` defaults to empty.** Until the provider's `grant_types` + contains `"authorization_code"` (enum value with underscore), the token + endpoint rejects every exchange with `invalid_request`. +- **Token endpoint is form-encoded + Basic-auth only.** A JSON body makes + `parse_token_request` see no `grant_type` → `unsupported_grant_type`; + POST-body client secrets are not accepted — authenticate the confidential + client with `Authorization: Basic base64(client_id:client_secret)`. +- **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`, + `grafana-profile-scope`, Python-syntax expressions) and will start emitting + claims the moment the evaluator bug is fixed — no config change needed then. + ## Configuration (secrets) Two secrets live in the host `.env` (git-ignored; placeholders in diff --git a/serverAppConfigs-docs/Infrastructure/Grafana.md b/serverAppConfigs-docs/Infrastructure/Grafana.md index ee351c2..d2f7589 100644 --- a/serverAppConfigs-docs/Infrastructure/Grafana.md +++ b/serverAppConfigs-docs/Infrastructure/Grafana.md @@ -1,6 +1,6 @@ # Grafana -Grafana visualizes the metrics that Prometheus collects from every service. It runs on Dankclaw and is reachable at `grafana.franksdank.cloud` (via Caddy, with basicauth). +Grafana visualizes the metrics that Prometheus collects from every service. It runs on Dankclaw and is reachable at `grafana.franksdank.cloud` (via Caddy, behind the Anubis challenge + embedded-outpost gate; login happens inside Grafana via the Authentik OAuth2 button). **Host:** Dankclaw — **Hostname:** `grafana.franksdank.cloud` — **Port:** 3002 (internal) — **Docs:** https://grafana.com/docs @@ -38,6 +38,17 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`. - Change the default admin credentials at first boot (env or UI). - Dashboards provisioned as YAML 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]] → "OAuth2 relying-party quirks". Consequences for Grafana: + users auto-upsert 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. ## See Also