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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 = <sub hex>`; 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user