Zurück zum Blog
·von Patrick Hofmann

Der IdP schützt den User — auch vor den SPs

Ein Service Provider lädt sein Logo hoch und kann damit auf der Consent-Seite den User über die Identität täuschen. Eine Notiz aus einem Hardening-Sprint, in dem ich erkannt habe, dass der schutzwürdige Akteur nicht die SP ist.

OpenApeIdentitySecurityBuilding in Public

Ein Service Provider publiziert seine Metadata via DDISA — Logo-URL, App-Name, Redirect-URIs in einem DNS-veröffentlichten Manifest. Beim nächsten User, der sich von dort durchklickt, steht auf der Consent-Seite "Anmelden bei trusted-bank-services" mit einem Logo, das aussieht wie eine bekannte Bank. Es ist keine Bank. Es ist ein beliebiger SP, der den User über seine Identität täuscht. Phishing, durch die Vordertür der Consent-Seite.

Das war nicht hypothetisch. Die Mechanik war drin. Ich habe sie gestern rausgenommen.

Wie ich es vorher gedacht habe

Der erste Bauplan eines IdP geht implizit davon aus: SPs sind Konsumenten. Sie publizieren ihre Metadata, sie holen sich Tokens ab, sie schicken den User durch die Auth-Flow und bekommen am Ende eine Identity zurück. Der IdP hat zwei Rollen — er authenticatet den User, und er bedient die SPs.

In diesem Bild ist Härtung eine Frage gegen missbräuchliche SPs: gegen Replay-Attacks, gegen Quota-Abuse, gegen Lateral-Movement zwischen Tenants. Klassische API-Härtung. Der SP ist der potenzielle Angreifer auf den IdP-Service.

Bei DDISA verschärft sich das Bild. SPs registrieren sich nicht — jede Domain auf der Welt kann ihre Metadata als DNS-Manifest publizieren und damit als SP auftauchen, ohne dass der IdP davon vorher weiß. Es gibt kein Vorab-Vetting, keinen Approval-Schritt vor dem ersten Auth-Request. Das macht den Filter beim Durchreichen der SP-Metadata zum User nicht weniger wichtig, sondern wichtiger.

Das Bild ist nicht falsch. Es ist nicht vollständig.

Was es übersieht: der User vertraut dem IdP, nicht dem SP. Der User hat eine Beziehung zum IdP — er hat dort sein Konto, er kennt das Branding, er erwartet, dass der IdP ihm sagt, womit er sich gerade verbindet. Der SP ist eine Drittpartei, der gegenüber der User keine direkte Trust-Relation hat. Er kommt nur via IdP-Vermittlung in den Kontext.

Wenn der IdP also einfach durchreicht, was der SP an Metadata liefert, hat der SP einen Kanal, durch den er den User direkt manipulieren kann. Der IdP wird zum Megafon.

Was die SP machen kann, wenn man sie lässt

Das Logo ist das offensichtliche Beispiel. Es gibt mehr.

javascript:-URIs in der Metadata. Ein SP gibt eine URI im javascript:-Schema an. Wenn der IdP diese URI als Link auf einer Auth-Flow-Seite einbaut — und sei es nur ein "zurück zur Anwendung"-Button für den Fehlerfall — hat der SP XSS in der vertrauenswürdigen IdP-Domain.

Externe Logo-URLs. Ein SP gibt als Logo-URL https://tracker.evil.com/pixel.png an. Der IdP rendert das auf der Consent-Seite als <img src>. Jeder User, der durch die Consent-Seite geht, lädt das Pixel — der SP weiß, welche User ihn überhaupt zu sehen bekommen, bevor sie je auf "Approve" geklickt haben.

Beliebige redirect_uri. Ein SP publiziert https://legitimate-app.example.com/callback als gültigen Callback in seinem DDISA-Manifest. Im Auth-Request übergibt er aber redirect_uri=https://attacker.com/steal. Wenn der IdP nicht gegen die publizierte Metadata validiert, sondern dem Request-Parameter glaubt, fließen Tokens zum Angreifer.

Passkey-Graft. Eine Variante auf User-Ebene: ein unauthenticated add-credential-Endpoint erlaubt es, an ein bestehendes Konto einen weiteren Passkey ranzuhängen. Ohne harte Session-Auth davor kann ein Angreifer einen eigenen Passkey ans Konto grafen — und hat danach legitimen Dauer-Zugang.

Jede dieser Lücken ist plausibel. Jede ist nutzbar. Keine ist exotisch.

Die Inversion

Der Punkt, der beim Schließen dieser Issues klar wurde: das ist nicht Härtung gegen die SPs. Das ist Härtung für die User gegen die SPs.

Eine andere Trust-Boundary. Nicht zwischen IdP und SP — wo SP der potenzielle Angreifer auf den IdP-Service ist — sondern zwischen User und SP, mit dem IdP als Vermittler in der Mitte. Der IdP filtert, was der SP zum User durchreicht.

Konkret im Code: das, was der User auf der Consent-Seite sieht, ist nicht mehr eine 1:1-Projektion der SP-Metadata.

// vorher: was die SP geliefert hat, war was der User sah
const consentView = {
  appName:     sp.metadata.name,
  logoUrl:     sp.metadata.logo,         // beliebige Quelle
  redirectUri: req.query.redirect_uri,   // beliebige URI
};

// jetzt: nichts SP-supplied geht ungeprüft an den User
const consentView = {
  appName:     sanitize(sp.metadata.name),
  logoUrl:     null,                     // SP-supplied logos: dropped
  redirectUri: matchRegistered(
    req.query.redirect_uri,
    sp.metadata.redirect_uris,           // exakter Eintrag oder Reject
  ),
};

Logos werden komplett gedroppt — es gibt im Moment keine kuratierte Allowlist, also gibt es kein Logo. URIs müssen https://-Schema haben, alles andere wird beim Metadata-Ingest abgelehnt. redirect_uri aus dem Auth-Request muss exakt einem in der publizierten Metadata gelisteten Eintrag entsprechen. add-credential läuft nur mit existierender authentifizierter Session.

Der SP ist nicht der Kunde des IdP. Der User ist der Kunde des IdP. Der SP ist eine Drittpartei, der gegenüber der IdP eine Schutzpflicht hat — gegenüber dem User.

Die sichtbare Konsequenz ist eine Default-Änderung: die Policy für neue SP-Anbindungen ist nicht mehr open (jeder SP, der die Metadata-Dance durchläuft, kann sofort User authenticaten), sondern consent (jeder neue SP muss explizit vom User-Owner zugelassen werden, bevor er User-Sessions kriegt).

open war der alte Default, weil ich an SPs als Konsumenten gedacht habe — je geringer die Reibung beim Anbinden, desto besser. consent ist der neue Default, weil ich an SPs als potenzielle Angreifer auf User gedacht habe — je expliziter die Zulassung, desto kontrollierter der Trust-Layer.

Das ist nicht eine kleine Konfig-Änderung. Es ist eine Aussage darüber, wem der IdP gehört.

Eine Richtung

Logos gedroppt, URIs validiert, redirect_uri gegen die Metadata gemappt, Passkey-Graft geschlossen, ein Hardening-Batch, Default geflippt. Keine davon ist ein dramatisches Architektur-Refactoring — jede einzelne ist ein paar Zeilen Code, ein zusätzlicher Filter, ein gestrichener Pfad.

Was sie zusammenhält, ist das Bild dahinter: welche Partei ist die schutzwürdige?

Wenn diese Frage falsch beantwortet ist, baut man die Filter an der falschen Stelle. Man härtet gegen Replay (die SP nervt mich) und vergisst das Logo (die SP belügt meine User). Beides sind reale Angriffe, aber sie haben unterschiedliche Opfer.

Wer ist der Kunde

Ein IdP fühlt sich anfangs an wie ein Service für SPs. Sie sind die, die die API benutzen, OAuth-Flows triggern, Tokens abholen. Die, die Dokumentation lesen und Tickets aufmachen. Sie sind sichtbar.

Der User taucht in diesem Bild kaum auf. Er klickt auf der Consent-Seite "Approve" und ist weg. Er sieht das Logo, sieht den App-Namen, sieht eine Permissions-Liste — das war's. Er ist user agent, nicht Kunde.

Aber er ist der einzige Akteur, der wirklich verloren hat, wenn etwas schiefläuft. Der SP, der ein Logo missbraucht hat, hat im Worst Case seinen Account gesperrt bekommen. Der User, der auf das Logo reingefallen ist, hat im Worst Case seine Identity verloren — die er nicht aussperren kann, weil sie seine ist.

Der IdP ist nicht der Diener der SPs. Er ist der Vermittler zwischen User und SP. Und in dieser Mittlerrolle gehört seine Loyalität zu der Partei, die im Schadensfall nicht zurück kann.


Code: github.com/openape-ai/openape, MIT-lizenziert.