feat(oidc): Profilbild vom Identitätsanbieter übernehmen

Schickt der Anbieter ein `picture`, wird es bei der Anmeldung geholt,
mittig auf 256x256 zugeschnitten und oben rechts angezeigt. Fehlt es
oder scheitert das Holen, bleibt es beim Buchstabenkreis - die
Anmeldung selbst hängt nicht davon ab.

Die Bildadresse kommt aus einem Token, also von außen. Erlaubt ist
deshalb ausschließlich der Ursprung aus OIDC_ISSUER, Weiterleitungen
eingeschlossen; dazu 4 MB und 5 Sekunden als Deckel. Ohne das wäre es
eine Aufforderung an den Server, für einen Fremden beliebige Adressen
im internen Netz anzufragen.

Neu: Migration 000019 (avatar_key/-source/-updated_at), Paket
internal/avatar, Route GET /api/v1/users/{id}/avatar. Der GC durchsucht
"avatars/" mit und kennt die Schlüssel - sonst räumte er die Bilder
nach einem Tag weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-24 13:43:22 +02:00
1 parent 3019e84681
commit 9f44215698
18 files changed
+814 -26

No files matched your search

+35
View File
@@ -1274,6 +1274,41 @@ anbieten. Anlass: Bei einem Bild stand nirgends, wie groß es in **Pixeln** ist.
---
## Profilbild vom Identitätsanbieter (24.08.2026)
Gemessen an der laufenden Instanz auf `127.0.0.1:8090`:
| Fall | Erwartet | Gemessen |
|---|---|---|
| Bild hinterlegt, `/me` | `avatar_ver` > 0 | 1787571663 |
| `GET /users/<id>/avatar?v=…` | `200`, `image/png`, 256×256 | ✅, `private, max-age=604800, immutable` |
| ohne Anmeldung | `401` | ✅ |
| Konto ohne Bild | `404` | ✅ |
| erfundene Konto-ID | `404`, nicht `403` | ✅ |
| Blob fehlt, Zeile steht | Buchstabenkreis, kein Loch | ✅ (Screenshot) |
| GC-Lauf | Bild bleibt, 0 Waisen | ✅ (122 gescannt) |
| verwaiste Datei unter `avatars/` | wird gefunden, Schonfrist greift | ✅ (123 gescannt, 1 jung) |
Geprüft mit Go-Tests, nicht am laufenden Anbieter:
- Zuschnitt: 800×200, 200×800, 512×512 und 16×16 ergeben alle 256×256; bei
einem breiten Bild bleibt die Mitte stehen (linke Kante rot, rechte blau).
- JPEG wird angenommen, Unsinn (`""`, `<svg/>`, Text) abgelehnt.
- **SSRF:** `169.254.169.254`, `localhost:8080`, `postgres:5432`,
`id.example.com.angreifer.net`, `id.example.com:9200`, `file://`, `gopher://`
werden alle abgewiesen; eine Weiterleitung auf einen fremden Rechner wird
nicht befolgt.
- Ein Anbieter, der erst `401` sagt, bekommt genau einen zweiten Versuch mit
Token – nicht mehr.
- Eine 8-MB-Antwort wird bei 4 MB abgeschnitten.
**Noch offen (braucht deinen echten PocketID):**
- [ ] Meldet PocketID `picture` im ID-Token oder erst über UserInfo?
- [ ] Kommt das Bild ohne Token heraus, oder greift der zweite Versuch?
- [ ] Bild in PocketID austauschen → steht nach dem nächsten Anmelden hier
- [ ] Bild in PocketID entfernen → das alte bleibt hier stehen (so gebaut:
gelöscht wird nur beim Austausch, nicht auf Verdacht)
## Automatisiert geprüft (22.08.2026)
Diese Punkte hat Claude per API/`curl` gegen Wegwerf-Konten durchgespielt; sie