electric-horses-infra/docs/adr/0007-oidc-avatar-data-url-gotcha.md
Benjamin Weinlich 09925f7eda docs(adr): add ADR-0007 on OIDC avatar data-URL gotcha
When a new user logs in via Authentik OIDC for the first time,
Forgejo tries to fetch the 'picture' claim as an avatar — but
Authentik delivers a 'data:image/svg+xml;base64,...' URL that
Forgejo can't store. Result: DB has an avatar hash but no file,
so /avatars/<hash> returns 404, the <img> is in broken state,
and the activity page's canvas renderer crashes with
'drawImage on broken state'.

Fix (per user, after first login):
  UPDATE "user" SET avatar = '', use_custom_avatar = false
  WHERE lower_name = '<name>';

Triggers Forgejo's default identicon generation, which works.

This commit:
- Adds ADR-0007 with full root cause + three rejected alternatives
- Updates docs/adr/README.md index
- Extends stacks/forgejo/Agent.md 'Known Gotchas' with the fix
- Appends the fix to docs/runbooks/forgejo-admin-recovery.md

Applied for user 'bw' already on 2026-04-12.

Refs OP#1119
2026-04-12 04:47:25 +02:00

123 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0007: OIDC-Avatar als data-URL bricht Forgejo-Canvas-Renderer
**Status:** Accepted (Lesson Learned)
**Datum:** 2026-04-12
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment (Post-Launch Fix)
## Kontext
Nach dem Silent-SSO-Fix (ADR-0006) tauchte ein JavaScript-Fehler auf der Forgejo-Activity-Seite auf:
```
JavaScript error: Failed to execute 'drawImage' on 'CanvasRenderingContext2D':
The HTMLImageElement provided is in the 'broken' state.
(https://code.sdda.eu/assets/js/index.js?v=10.0.3~gitea-1.22.0 @ 19:16870)
```
Betroffen war die Seite `/<org>/<repo>/activity`, wo Forgejo eine Heatmap der Commit-Aktivität via HTML5 Canvas rendert. Der Canvas-Code versucht das Avatar-Bild jedes Committers mit `ctx.drawImage(img, ...)` in die Graphik zu zeichnen. Wenn das Avatar-Bild im `broken state` ist (Image-Load failed, z.B. 404), crasht `drawImage` und der Renderer bricht ab — Seite halb tot, JS-Error im Footer.
## Root Cause
### Was Forgejo beim ersten OIDC-Login macht
Wenn ein User zum ersten Mal via OIDC einloggt, holt Forgejo:
1. Die Claims `name`, `email`, `preferred_username` aus dem ID-Token
2. Das Feld `picture` aus den userinfo-Claims (OIDC-Standard für Avatar-URL)
Forgejo versucht dann das Avatar zu **downloaden und lokal zu cachen** unter `/data/gitea/avatars/<hash>`. Der Hash wird aus Email+UserId deterministisch berechnet (MD5-basiert für Gravatar-Kompatibilität).
### Was Authentik liefert
Authentik's Default-Scope-Mapping für `profile` setzt den `picture`-Claim auf einen **`data:image/svg+xml;base64,...`** String. Das ist Authentiks auto-generiertes Text-Avatar (die zwei Buchstaben des Users auf farbigem Hintergrund).
Das ist technisch ein valides OIDC-Claim — OIDC-Spec erlaubt `picture` als URL-String, macht aber keine Aussage über das Protokoll. `data:`-URLs sind URIs, keine URLs im engeren Sinn.
### Was Forgejo daraus macht
Der Go-Code in Forgejo (`modules/migration/downloader.go` und `services/auth/source/oauth2/source_sync.go`) erwartet eine **HTTP-URL** und versucht mit `http.Get()` das Bild zu ziehen. Bei einem `data:`-URI:
- Entweder schlägt der HTTP-Request fehl (kein HTTP-Handler für `data:` Scheme) und Forgejo ignoriert den Fehler stillschweigend
- Oder Forgejo speichert trotzdem den Hash in der DB-Spalte `user.avatar`, ohne dass die Datei auf Disk existiert
**Ergebnis:** `user.avatar = '7231c2c74257ae341118d44323c173f6'` in der DB, aber `/data/gitea/avatars/7231c2c74257ae341118d44323c173f6` ist **nicht vorhanden**. Die von Forgejo gerenderte URL `/avatars/<hash>` liefert dann 404, das HTML-`<img>`-Element geht in den broken state, und der Canvas-Renderer crasht.
## Entscheidung
**Kurzfristiger Fix (für bestehende Users):** DB-Feld `user.avatar` leeren. Forgejo fällt dann auf sein **internes Default-Identicon** zurück (bunte geometrische Muster aus User-ID), das on-the-fly generiert und gecacht wird.
**Mittelfristige Option (nicht umgesetzt):** Authentik-Scope-Mapping so anpassen dass der `picture`-Claim **entweder leer ist oder eine echte HTTP-URL** liefert (z.B. Gravatar-URL aus Email). Siehe Option B unten.
## Umsetzung
### Fix für bestehenden User
```sql
-- Auf ai-apps, im forgejo-db Container
UPDATE "user"
SET avatar = '',
use_custom_avatar = false
WHERE lower_name = '<username>';
```
Nach dem Update liefert `https://code.sdda.eu/avatars/<hash>` HTTP 200 mit einem auto-generierten Identicon. Die URL bleibt stabil (deterministisch aus Email+UserID), nur der Inhalt ist jetzt das Default-Bild.
### Präventiv für neue Users
Das Problem tritt bei **jedem neuen OIDC-User beim ersten Login** wieder auf. Sobald Leonard, Martin oder neue Teammitglieder zum ersten Mal via `code.sdda.eu → Anmelden mit authentik` kommen, ist ihr Avatar wieder broken.
**Quick-Fix für neue User:** Gleiches SQL-Update anwenden sobald der User angelegt ist (siehe Runbook).
**Automatisierter Fix (nicht implementiert):** Ein Cron oder Directus Flow der alle neuen User mit nicht-existierenden Avatar-Dateien automatisch bereinigt. Würde aussehen wie:
```sql
UPDATE "user"
SET avatar = '', use_custom_avatar = false
WHERE avatar != ''
AND use_custom_avatar = false
AND NOT EXISTS (
-- Pseudo-Check: gibt es die Datei?
-- Müsste extern geprüft werden, SQL kann kein Filesystem
);
```
Das geht in pure SQL nicht, müsste ein Shell-Script sein.
## Alternativen (nicht gewählt)
### Option A: Avatar komplett ausschalten
Via `app.ini`:
```ini
[service]
ENABLE_USER_HEATMAP = false
```
Das würde den Canvas-Renderer nicht aktivieren und der Fehler käme nicht vor. **Verworfen** weil die Activity-Heatmap ein nützliches Feature ist das wir behalten wollen.
### Option B: Authentik picture-Claim als echte URL setzen
Ein Custom Property-Mapping in Authentik, z.B.:
```python
# In Authentik → Customization → Property Mappings → Scope Mapping "profile"
# Override für das "picture" field:
return "https://gravatar.com/avatar/" + hashlib.md5(request.user.email.lower().encode()).hexdigest() + "?d=identicon"
```
Das würde eine echte HTTP-URL liefern. **Verworfen** weil:
1. Wir wollen nicht, dass Emails (auch gehasht) an Gravatar/Automattic gehen (DSGVO)
2. Der Workaround ist simpler und effektiv
3. Eingriff in Authentik-Scope-Mappings betrifft alle Authentik-Integrationen, nicht nur Forgejo — zu viel Blast-Radius für ein kosmetisches Issue
### Option C: Avatar-Datei manuell erzeugen
Ein Shell-Script das für jeden broken User ein 128×128 PNG generiert und unter `/data/gitea/avatars/<hash>` ablegt. **Verworfen** als over-engineered für ein dauerhaftes Problem — wenn die DB geflushed wird (Restore, Re-Login), entsteht der Bug wieder.
## Konsequenzen
### Positiv
- Avatar-Canvas-Renderer funktioniert wieder
- Keine weiteren Code-Änderungen nötig — pure Daten-Bereinigung
- Default-Identicons sehen vertretbar aus (farbig, einzigartig pro User)
- Lösung ist trivial reproduzierbar: ein SQL-Update pro User
### Negativ / Trade-off
- **Neue User erleben den Bug weiterhin** bis das SQL-Update angewandt wird. Solange es nur vereinzelte Logins sind ist das tragbar.
- Die Authentik-UI zeigt ein hübsches Text-Avatar mit User-Initialen; im Forgejo sieht man statt dessen das geometrische Identicon. Geringe visuelle Inkonsistenz zwischen den Apps.
- Kein permanenter Fix in Forgejos Code-Pfad — ein späteres Forgejo-Upgrade könnte theoretisch das Verhalten ändern und einen anderen Workaround erfordern.
## Follow-Up-Ideen
- Eigenes Avatar manuell hochladen: `Profile → Einstellungen → Konto → Custom Avatar`. Würde `use_custom_avatar=true` setzen und ein echtes Bild verlinken. Einmalige Aktion pro User — wahrscheinlich die cleanste Langzeit-Lösung.
- Ein Forgejo-Hook / Script das nach jedem neuen User-Create automatisch das SQL-Update macht. Siehe Runbook-Update.
- Upstream-Issue im Forgejo-Repo öffnen: "OIDC `picture` claim mit `data:`-URL sollte gracefull handled werden, nicht zu broken DB-State führen".
## Related
- [ADR-0003](0003-native-oidc-not-forwardauth.md) — Warum wir nativ OIDC machen (ohne OIDC gäbe es dieses Issue nicht)
- [ADR-0006](0006-silent-sso-launch-url.md) — Silent-SSO Fix, der den ersten OIDC-Login überhaupt möglich gemacht hat
- [Runbook: forgejo-admin-recovery.md](../runbooks/forgejo-admin-recovery.md) — wird um den Avatar-SQL-Fix ergänzt
- `stacks/forgejo/Agent.md` — "Known Gotchas" Sektion wird um dieses Issue ergänzt