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

7.3 KiB
Raw Blame History

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

-- 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:

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:

[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.:

# 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".
  • ADR-0003 — Warum wir nativ OIDC machen (ohne OIDC gäbe es dieses Issue nicht)
  • ADR-0006 — Silent-SSO Fix, der den ersten OIDC-Login überhaupt möglich gemacht hat
  • Runbook: 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