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
This commit is contained in:
parent
20025814e8
commit
09925f7eda
4 changed files with 147 additions and 2 deletions
123
docs/adr/0007-oidc-avatar-data-url-gotcha.md
Normal file
123
docs/adr/0007-oidc-avatar-data-url-gotcha.md
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
# 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
|
||||||
|
|
@ -45,6 +45,7 @@ Fortlaufend `NNNN` beginnend bei `0001`. **Niemals** alte ADRs ändern — super
|
||||||
| 0004 | [Subdomain code.sdda.eu](0004-subdomain-code-sdda-eu.md) | Accepted |
|
| 0004 | [Subdomain code.sdda.eu](0004-subdomain-code-sdda-eu.md) | Accepted |
|
||||||
| 0005 | [Volume-Mount auf /data](0005-volume-mount-data-not-var-lib.md) | Accepted (Lesson Learned) |
|
| 0005 | [Volume-Mount auf /data](0005-volume-mount-data-not-var-lib.md) | Accepted (Lesson Learned) |
|
||||||
| 0006 | [Silent SSO via OAuth2-Initiate-Launch-URL](0006-silent-sso-launch-url.md) | Accepted (Lesson Learned) |
|
| 0006 | [Silent SSO via OAuth2-Initiate-Launch-URL](0006-silent-sso-launch-url.md) | Accepted (Lesson Learned) |
|
||||||
|
| 0007 | [OIDC-Avatar als data-URL bricht Canvas-Renderer](0007-oidc-avatar-data-url-gotcha.md) | Accepted (Lesson Learned) |
|
||||||
|
|
||||||
## Vorlage für neue ADRs
|
## Vorlage für neue ADRs
|
||||||
Copy-and-paste Startpunkt:
|
Copy-and-paste Startpunkt:
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,22 @@ Wenn Authentik auf Dauer nicht mehr funktioniert (z.B. Lizenz-Issue, Migration n
|
||||||
4. Neuer Auth-Provider später einrichten
|
4. Neuer Auth-Provider später einrichten
|
||||||
5. Doku in neue ADR schreiben
|
5. Doku in neue ADR schreiben
|
||||||
|
|
||||||
|
## Zusatz: Neue OIDC-User Avatar-Fix
|
||||||
|
|
||||||
|
**Wann anwenden:** Direkt nachdem ein neuer User zum ersten Mal via Authentik eingeloggt hat (Leonard, Martin, etc.).
|
||||||
|
|
||||||
|
**Grund:** Forgejo speichert beim OIDC-First-Login einen Avatar-Hash in der DB, aber die eigentliche Bilddatei ist leer (weil Authentik ein `data:`-URL statt einer echten HTTP-URL im `picture`-Claim liefert). Resultat: `/avatars/<hash>` gibt 404, und die Activity-Seite crasht mit einem `drawImage on broken state` Fehler. Siehe [ADR-0007](../adr/0007-oidc-avatar-data-url-gotcha.md).
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
ssh ai-apps
|
||||||
|
docker exec forgejo-db psql -U forgejo -d forgejo -c \
|
||||||
|
"UPDATE \"user\" SET avatar = '', use_custom_avatar = false WHERE lower_name = '<new-username>';"
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach generiert Forgejo ein Default-Identicon on-the-fly, die Activity-Seite funktioniert wieder. Der User kann später im Web-UI ein eigenes Avatar hochladen (Einstellungen → Konto → Profilbild).
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
- `Agent.md` — Stack-Overview
|
- `../../stacks/forgejo/Agent.md` — Stack-Overview mit Known Gotchas
|
||||||
- `../Authentik/howto-oauth2-provider.md` — Wie OIDC-Provider neu aufgesetzt wird
|
- [`../guides/authentik-oauth2-provider.md`](../guides/authentik-oauth2-provider.md) — Wie OIDC-Provider neu aufgesetzt wird
|
||||||
|
- [ADR-0007](../adr/0007-oidc-avatar-data-url-gotcha.md) — Background zum Avatar-Issue
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,11 @@ Du arbeitest am Forgejo-Git-Hosting. Lies erst `../../Agent.md` am Repo-Root fü
|
||||||
3. **OIDC-Config lebt in der Postgres-DB** (Tabelle `login_source`), NICHT in `app.ini`. Zum Ändern: `docker exec -u git forgejo sh -c 'cd / && forgejo admin auth update-oauth --id 1 ...'`
|
3. **OIDC-Config lebt in der Postgres-DB** (Tabelle `login_source`), NICHT in `app.ini`. Zum Ändern: `docker exec -u git forgejo sh -c 'cd / && forgejo admin auth update-oauth --id 1 ...'`
|
||||||
4. **`forgejo admin` CLI** braucht `-u git` und `cd /`: `docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'`
|
4. **`forgejo admin` CLI** braucht `-u git` und `cd /`: `docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'`
|
||||||
5. **User-Promotion zum Admin** via SQL am saubersten: `UPDATE "user" SET is_admin = true WHERE lower_name = 'NAME';`
|
5. **User-Promotion zum Admin** via SQL am saubersten: `UPDATE "user" SET is_admin = true WHERE lower_name = 'NAME';`
|
||||||
|
6. **Neuer OIDC-User → broken Avatar:** Authentik liefert das Avatar als `data:image/svg+xml;base64,...` Claim, Forgejo kann das nicht speichern → `drawImage` crasht auf Activity-Seite. Siehe ADR-0007. Fix nach jedem neuen First-Login:
|
||||||
|
```sql
|
||||||
|
UPDATE "user" SET avatar = '', use_custom_avatar = false
|
||||||
|
WHERE lower_name = '<new-user>';
|
||||||
|
```
|
||||||
|
|
||||||
## Ops-Kommandos
|
## Ops-Kommandos
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue