# 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 `///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/`. 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/` liefert dann 404, das HTML-``-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 = ''; ``` Nach dem Update liefert `https://code.sdda.eu/avatars/` 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/` 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