1 Punkte von GN⁺ 4 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Viele CLIs verwenden standardmäßig einen localhost-OAuth-Redirect, der im lokalen Browser auf dem Laptop schnell abgeschlossen ist, doch in Entwicklungsumgebungen wie SSH, Containern oder WSL bricht diese Annahme zusammen und der Login-Flow bleibt hängen
  • Das aktuelle Verfahren funktioniert so, dass die CLI einen temporären HTTP-Server auf 127.0.0.1 öffnet, den Browser zur Authentifizierungs-URL schickt und der Authentifizierungsanbieter dann den Authorization Code an den lokalen Callback zurückliefert
  • Der 2019 standardisierte RFC 8628 Device Authorization Grant trennt die CLI, die das Token anfordert, von dem Browser-Gerät, auf dem sich der Nutzer authentifiziert, und beseitigt so die Abhängigkeit von Port-Binding oder einem lokalen Browser
  • Der Device Flow erhält device_code, user_code, verification_uri und interval, pollt periodisch /token und verarbeitet standardisierte Status wie authorization_pending, slow_down, access_denied und expired_token
  • Für neue CLIs sollte der Device Flow die Standardeinstellung sein, Endpunkte sollten über .well-known/openid-configuration entdeckt werden, und Refresh Tokens sollten nicht in einer JSON-Datei unter ~/.config, sondern im OS-Keychain gespeichert werden

Was localhost-Redirects voraussetzen

  • Ein üblicher CLI-Login basiert auf der Annahme, dass lokaler HTTP-Server und Systembrowser auf derselben Maschine laufen
    • Die CLI bindet einen HTTP-Server an einen bestimmten Port auf 127.0.0.1
    • Sie öffnet den Systembrowser am OAuth-Authorization-Endpoint und übergibt redirect_uri=http://127.0.0.1:<port>/callback
    • Nach dem Login leitet der Authentifizierungsanbieter den Authorization Code per 302 an die Loopback-URL weiter
    • Der kleine HTTP-Server der CLI liest den Code aus und tauscht ihn am Token-Endpoint gegen Tokens ein
    • Meist kommt PKCE zum Einsatz, danach erscheint eine Seite wie „Diesen Tab können Sie jetzt schließen“
  • gcloud auth login, wrangler login, das frühere vercel login und viele Vendor-CLIs verwenden diesen Ansatz
    • Wrangler verwendet Port 8976
    • gcloud verwendet 8085
    • Claude Code belegt bei jedem Start einen temporären Port
  • RFC 8252 empfiehlt dieses Muster für Native Apps, wenn ein Browser auf dem Host vorhanden ist, behandelt aber nicht den Fall, dass auf dem Host kein Browser existiert

Warum Nutzer den localhost-Schritt oft nicht bemerken

  • Der localhost-Callback läuft so kurz ab, dass ihn die meisten Nutzer nie sehen
  • Die von der CLI ausgegebene URL ist lang und enthält die Redirect-URI in der Query-String
  • Nutzer loggen sich auf der tatsächlichen Domain des Authentifizierungsanbieters ein und erteilen dort ihre Zustimmung
  • Der Authentifizierungsanbieter schickt den Browser dann an den localhost-Callback, damit die CLI den Code auslesen kann, und leitet anschließend auf eine aufpolierte „signed in“-Seite weiter
  • Oberflächlich wirkt es wie „Ich habe mich auf einer Website eingeloggt und die CLI war danach authentifiziert“, tatsächlich trägt aber das Zusammenspiel von lokalem HTTP-Server und Browser den gesamten Flow

Wo es in SSH, Containern und WSL kaputtgeht

  • Der gesamte Flow hängt an der Annahme, dass die Maschine, auf der die CLI läuft, dieselbe ist wie die Maschine, auf der der Browser läuft
  • In einer SSH-Session gibt es auf dem Remote-Host keinen Browser, und xdg-open schlägt fehl oder öffnet in einer X-Forwarding-Umgebung einen unsichtbaren Remote-Browser
    • Man kann den Callback-Port zwar zum Laptop tunneln, aber die beim Authentifizierungsanbieter registrierte Redirect-URI muss den durch den Tunnel laufenden Port auch erlauben
  • In Containern gibt es keinen Browser, und viele Images enthalten weder xdg-open noch open
    • Mit -p kann man den Callback-Port exponieren, aber man muss wissen, welchen Port die CLI belegen wird
    • Beim Cloudflare-CLI gibt es dazu fortlaufend Issues von blockierten Nutzern
  • Unter WSL wird der Browser unter Windows geöffnet, während der Loopback-Server unter Linux läuft
    • Das Port-Forwarding von WSL2 funktioniert meistens, aber nicht immer
  • Auf gemeinsam genutzten Systemen können andere Prozesse auf derselben Maschine den Listening-Port über /proc/net/tcp finden oder versuchen, einen bekannten Port zuerst zu binden
    • PKCE schützt den Code-Austausch, aber nicht die authentifizierte Session des Redirects selbst

Dass es bereits Fallbacks gibt, zeigt das Designproblem

  • CLIs, die den Loopback-Flow standardmäßig anbieten, liefern meist auch einen Fallback für den Fehlerfall mit
  • gcloud hat --no-launch-browser
  • Wrangler bleibt hängen; der akzeptierte Workaround besteht darin, die localhost-URL in einem zweiten Terminal direkt mit curl aufzurufen
  • Anthropics claude gibt „Paste code here if prompted“ aus und wartet
  • Diese Fallbacks sind de facto ein manueller Device Flow und existieren, weil der Standard-Flow in realen Einsatzumgebungen der CLI nicht funktioniert

RFC 8628 Device Authorization Grant

  • RFC 8628 ist der 2019 veröffentlichte OAuth 2.0 Device Authorization Grant für „input-constrained devices“
    • Dazu zählen Fernseher, Konsolen und CLIs
    • Kernidee ist die Trennung zwischen dem Gerät, das das Token anfordert, und dem Gerät, auf dem der Nutzer die Authentifizierung durchführt
  • Die CLI sendet einen POST an den device_authorization_endpoint des Authentifizierungsanbieters
    • Eine Beispielanfrage übermittelt client_id=my-cli&scope=openid+offline_access
  • Der Authentifizierungsanbieter antwortet mit JSON, das folgende Werte enthält
    • device_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • Die CLI gibt URL und kurzen Code aus und zeigt nach Möglichkeit zusätzlich einen QR-Code für verification_uri_complete an
  • Der Nutzer öffnet die URL auf einem beliebigen Gerät, loggt sich ein, prüft anhand des angeforderten Scope und des Client-Namens, ob der kurze Code mit dem in der CLI angezeigten übereinstimmt, und bestätigt dann den Zugriff

Polling und standardisierte Statusverarbeitung

  • Die CLI pollt den Token-Endpoint alle interval Sekunden
  • Als Grant Type wird urn:ietf:params:oauth:grant-type:device_code verwendet
  • RFC 8628 Abschnitt 3.5 definiert die folgenden Status
    • authorization_pending: Es wird noch auf die Bestätigung des Nutzers gewartet
    • slow_down: Der Authentifizierungsanbieter verlangt ein langsameres Polling; die Spezifikation schreibt vor, das Intervall um mindestens 5 Sekunden zu erhöhen
    • access_denied: Der Nutzer hat abgelehnt
    • expired_token: Das Token ist nach zu langem Warten abgelaufen
  • Im Device Flow bindet die CLI keinen Port und setzt nicht voraus, dass auf dem Ausführungshost ein Browser vorhanden ist
  • Dieselbe Login-Methode funktioniert damit auf dem Laptop, im Container und in einem CI-Job, der auf eine menschliche Freigabe wartet

Polling-Kosten und Endpoint-Discovery

  • Das Standard-Polling-Intervall beträgt 5 Sekunden
  • Die meisten Authentifizierungen sind innerhalb einer Minute abgeschlossen, daher pollt ein gewöhnlicher Login /token ungefähr zehnmal und endet dann
  • Der Server kann das Intervall über slow_down erhöhen, und sauber implementierte Clients müssen das befolgen
  • Verglichen mit einem stateful Endpoint, der pro ausstehendem Login eine WebSocket- oder SSE-Verbindung offen hält, ist zustandsloses Polling auf /token einfacher und günstiger
  • Wenn der Authentifizierungsanbieter OpenID Connect Discovery unterstützt, kann die CLI device_authorization_endpoint und token_endpoint aus .well-known/openid-configuration beziehen und muss keine URLs hart kodieren

Das Phishing-Risiko des Device Flow

  • Beim Device Flow kann ein Angreifer den device_authorization_endpoint eines echten Authentifizierungsanbieters aufrufen, user_code und device_code erhalten und das Opfer dann zur Eingabe bewegen
  • Das Opfer kann sich auf der echten URL mit dem echten Code anmelden und den echten Consent Screen bestätigen
  • Der Angreifer pollt währenddessen mit dem von ihm erzeugten device_code /token und erhält schließlich ein Access Token
  • Russische Threat Actors führen seit August 2024 eine solche Kampagne gegen M365-Tenants durch
    • Microsoft Threat Intelligence verfolgt sie als Storm-2372
    • Volexity ordnet sie APT29/Midnight Blizzard zu
    • Betroffen sind Regierungs-, Verteidigungs- und NGO-Tenants auf mehreren Kontinenten

Schutz vor Phishing ist Aufgabe des Authentifizierungsanbieters

  • Der Schutz vor Phishing muss nicht in der CLI, sondern beim Authentifizierungsanbieter stattfinden
  • Notwendige Gegenmaßnahmen sind unter anderem
    • kurze Ablaufzeiten für user_code
    • gut sichtbare Anzeige von Client-Namen und Anforderungsort auf der Verification Page
    • Rate Limiting für Versuche zur Code-Eingabe
    • kein Ausliefern von verification_uri_complete, damit Opfer den Code direkt eingeben müssen, statt nur auf einen Link zu klicken
    • Conditional-Access-Richtlinien, die den Device-Code-Flow bei hochwertigen Tenants blockieren, wenn Netzwerk oder Gerät nicht bekannt sind
  • Die Aufgabe der CLI ist es, die Spezifikation zu befolgen und keine Abkürzungen einzubauen
  • Der Device Flow verschiebt die Angriffsfläche von lokalen Angriffen hin zu Social-Engineering-Angriffen, liefert dafür aber einen Flow, der in deutlich mehr Umgebungen funktioniert und die Schutzmaßnahmen des Authentifizierungsanbieters nutzen kann

Kernelemente einer Go-Implementierung

  • Die vollständige Implementierung passt in Go mit net/http in etwa 30 Zeilen
  • Der Ablauf sieht wie folgt aus
    • http.PostForm an DeviceAuthorizationEndpoint mit client_id und scope
    • Dekodieren der JSON-Antwort mit DeviceCode, UserCode, VerificationURIComplete und Interval
    • Ausgabe von VerificationURIComplete und UserCode für den Nutzer
    • Wiederholte POST-Requests an TokenEndpoint mit device_code, client_id und dem Device-Grant-Type
    • Bei authorization_pending weiter warten
    • Bei slow_down das Intervall um 5 Sekunden erhöhen
    • Wenn kein Fehler vorliegt, access_token und refresh_token zurückgeben
    • Andere Fehler als Fehlschlag behandeln
  • Wenn man im Keycloak-Realm die Capability „OAuth 2.0 Device Authorization Grant“ aktiviert oder einen OpenID-zertifizierten Provider mit Unterstützung für diesen Grant verwendet, funktioniert der Device-Flow-Login

Was der Standard für neue CLIs sein sollte

  • Der Device Flow sollte die Standardeinstellung sein
  • Endpunkte sollten über .well-known/openid-configuration entdeckt und nicht hart kodiert werden
  • interval und slow_down müssen zwingend beachtet werden
  • Refresh Tokens gehören nicht in eine JSON-Datei unter ~/.config, sondern in den OS-Keychain
  • Wenn man für schnelle Laptop-Logins zusätzlich einen Loopback-Pfad anbieten will, sollte er hinter einem --web-Flag stehen und nicht der Standard sein

CLIs, die schon umgestiegen sind, und Werkzeuge, die noch fehlen

  • Es gibt CLIs, die den Device Flow standardmäßig verwenden
    • gh auth login nutzt den Device Flow von Anfang an und gilt im Open-Source-Bereich als eine der saubersten Referenzimplementierungen
    • aws sso login führt den Device Flow end-to-end gegen IAM Identity Center aus
    • vercel login ist im September 2025 zu RFC 8628 gewechselt und ersetzt damit den E-Mail-basierten Login sowie das frühere --oob-Flag
    • Die Stripe CLI nutzt zwar nicht direkt RFC 8628, bietet aber mit einem Pairing-Code-Flow eine gut umgesetzte UX
  • Andere Tools setzen weiterhin standardmäßig auf den Loopback-Flow und hängen einen „Code einfügen“-Fallback daran
    • Google gcloud
    • Cloudflare wrangler
    • Anthropic claude
  • Wenn eine CLI jedes Mal, wenn sie den Laptop verlässt, einen manuellen „Code einfügen“-Fallback braucht, sollte genau dieser Fallback der Standard-Flow sein

1 Kommentare

 
GN⁺ 4 시간 전
Lobste.rs-Kommentare
  • Etwas salopp formuliert, aber interessant. Wenn man Geräte-Codes/Links alle 1 Minute austauscht, könnte das wohl auch den Missbrauch für Phishing verringern.
    Nach der ersten Verwendung könnte man die Rotation stoppen und die Sitzung an IP oder Browser binden.

    • Das hilft vermutlich nicht so stark, wie im Artikel behauptet wird. Es ist ziemlich einfach, eine Phishing-Landingpage zu bauen, die beim Aufruf den Flow startet und sofort zum legitimen Anbieter weiterleitet.
      Bei Anbietern wie Microsoft, bei denen der Nutzer den Code selbst eingibt, könnte die Landingpage zudem Hinweise anzeigen und den Code in die Zwischenablage kopieren, wodurch man noch leichter auf Phishing hereinfällt.
  • Guter Artikel, und ich stimme zu, dass alle zu RFC 8628 wechseln sollten.
    Weil ich den CLI-OAuth-Prozess auf entfernten Entwicklungsmaschinen zu oft durchlaufen musste, habe ich ein persönliches Tool gebaut, das xdg-open abfängt und Ports automatisch weiterleitet, um die schlechte User Experience zu kaschieren: https://github.com/phinze/bankshot

  • Interessant. Ich habe erst kürzlich die „alte“ Authentifizierungsmethode RFC 8252 implementiert und wusste nichts von der „neuen“ RFC 8628.
    Vermutlich lag das an einer Wissenslücke, weil mein Hauptanwendungsfall die Authentifizierung gegen Google-Server war. In der Dokumentation, die ich für den RFC-8628-Flow hielt, steht Folgendes ausdrücklich:

    Alternatives

    If you are writing an app for a platform such as Android, iOS, macOS, Linux, or Windows (including the Universal Windows Platform), that has access to the browser and full input capabilities, use the OAuth 2.0 flow for mobile and desktop applications. (You should use that flow even if your app is a command-line tool without a graphical interface.)
    Deshalb habe ich einfach nur den RFC-8252-Flow gelesen und implementiert. Mein Tool ist zwar eine CLI, aber der Anwendungsfall ist rein lokal, daher habe ich SSH- oder Container-Umgebungen nicht berücksichtigt.
    Außerdem erlaubt Google im RFC-8628-Flow nur eingeschränkte OAuth-2.0-Scopes, was für manche Anwendungen eine entscheidende Einschränkung sein kann.

    • Kleine Korrektur: Ich habe die Nummer im Original noch einmal geprüft, es ist RFC 8628.
      Googles Scope-Einschränkung ist der Punkt, an dem OIDC unerquicklich wieder auftaucht. Idealerweise sollte Google ein ID-Token zurückgeben, statt alles in das Access-Token hineinzustopfen, aber das ist ein Problem von Googles OAuth-Konfiguration und keine Eigenschaft von 8628 selbst.
      Die endlose Komplexität von OAuth kommt genau daher. Der Standard definiert gut den Rahmen dafür, wie ein Autorisierungsschema aufgebaut und übertragen wird, schweigt aber absichtlich dazu, was es sein sollte. Selbst um ein gemeinsames Set von HTTP-Endpunkten zu bekommen, auf das sich „die meisten“ Anbieter einigen, brauchte es erst die Erfindung von OIDC und mehrere Jahre.
  • Ein weiterer Hack ist, den xdg-open-Aufruf des Servers an den Laptop weiterzuleiten. Ich habe dafür ein kleines Tool für meine persönliche Infrastruktur gebaut: https://github.com/zimbatm/subportal/

  • Könnte man die beiden Ansätze nicht kombinieren? Man leitet auf eine localhost-URL um und schickt hello zurück; wenn der Client hello nicht erhält, gibt die CLI die URL aus.
    Wenn der Server gleichzeitig keine Antwort auf das von ihm gesendete hello erhält, könnte der Browser einen Code anzeigen und eine Meldung wie „Bestätigen Sie, dass Sie gerade eine Anmeldung versuchen“ ausgeben. Man könnte es auch einfacher machen, etwa wie bei Google mit Zahlen, die man auf dem Telefon auswählt.

    cli -> server/auth?r=localhost&fallback_choices=10,20,30  
    server -> localhost/hello
    
    Case 1: hello request received, go to redirect URI on localhost  
    Case 2: server has not received a hello reply, client has not received a hello request
    - CLI displays a/the webpage url and prompts for selecting a fallback_choice
    - Webpage displays a number say `20` from choices
      - Warn in the webpage not to share this code
    - User enters/selects it on the CLI
      - solves the token copy/paste problem if choices  
    

    Der Vorteil ist, dass Menschen auch in Fall 2 zwar leicht auf einen Link klicken, OTP-/Code-Freigabe aber vergleichsweise seltener vornehmen, und dass ein Angreifer während des Hacks fortlaufend social-engineeren müsste.

  • Wenn es auf dem lokalen Rechner gut funktioniert, wäre es schön, wenn standardmäßig ein browserbasierter Flow genutzt würde, weil dann keine Interaktion nötig ist.

    • Dieser Flow funktioniert ebenfalls browserbasiert, wenn alles gut läuft. Er hat nur einen besseren Fallback-Pfad, wenn es fehlschlägt.