CLI-Authentifizierung, richtig gemacht
(abgeo.dev)- 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_uriundinterval, pollt periodisch/tokenund verarbeitet standardisierte Status wieauthorization_pending,slow_down,access_deniedundexpired_token - Für neue CLIs sollte der Device Flow die Standardeinstellung sein, Endpunkte sollten über
.well-known/openid-configurationentdeckt 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
302an 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“
- Die CLI bindet einen HTTP-Server an einen bestimmten Port auf
gcloud auth login,wrangler login, das früherevercel loginund viele Vendor-CLIs verwenden diesen Ansatz- Wrangler verwendet Port
8976 - gcloud verwendet
8085 - Claude Code belegt bei jedem Start einen temporären Port
- Wrangler verwendet 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-openschlä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-opennochopen- Mit
-pkann 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
- Mit
- 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/tcpfinden 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
curlaufzurufen - Anthropics
claudegibt „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_endpointdes Authentifizierungsanbieters- Eine Beispielanfrage übermittelt
client_id=my-cli&scope=openid+offline_access
- Eine Beispielanfrage übermittelt
- Der Authentifizierungsanbieter antwortet mit JSON, das folgende Werte enthält
device_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
- Die CLI gibt URL und kurzen Code aus und zeigt nach Möglichkeit zusätzlich einen QR-Code für
verification_uri_completean - 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
intervalSekunden - Als Grant Type wird
urn:ietf:params:oauth:grant-type:device_codeverwendet - RFC 8628 Abschnitt 3.5 definiert die folgenden Status
authorization_pending: Es wird noch auf die Bestätigung des Nutzers gewartetslow_down: Der Authentifizierungsanbieter verlangt ein langsameres Polling; die Spezifikation schreibt vor, das Intervall um mindestens 5 Sekunden zu erhöhenaccess_denied: Der Nutzer hat abgelehntexpired_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
/tokenungefähr zehnmal und endet dann - Der Server kann das Intervall über
slow_downerhö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
/tokeneinfacher und günstiger - Wenn der Authentifizierungsanbieter OpenID Connect Discovery unterstützt, kann die CLI
device_authorization_endpointundtoken_endpointaus.well-known/openid-configurationbeziehen und muss keine URLs hart kodieren
Das Phishing-Risiko des Device Flow
- Beim Device Flow kann ein Angreifer den
device_authorization_endpointeines echten Authentifizierungsanbieters aufrufen,user_codeunddevice_codeerhalten 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/tokenund 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
- kurze Ablaufzeiten für
- 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/httpin etwa 30 Zeilen - Der Ablauf sieht wie folgt aus
http.PostFormanDeviceAuthorizationEndpointmitclient_idundscope- Dekodieren der JSON-Antwort mit
DeviceCode,UserCode,VerificationURICompleteundInterval - Ausgabe von
VerificationURICompleteundUserCodefür den Nutzer - Wiederholte POST-Requests an
TokenEndpointmitdevice_code,client_idund dem Device-Grant-Type - Bei
authorization_pendingweiter warten - Bei
slow_downdas Intervall um 5 Sekunden erhöhen - Wenn kein Fehler vorliegt,
access_tokenundrefresh_tokenzurü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-configurationentdeckt und nicht hart kodiert werden intervalundslow_downmü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 loginnutzt den Device Flow von Anfang an und gilt im Open-Source-Bereich als eine der saubersten Referenzimplementierungenaws sso loginführt den Device Flow end-to-end gegen IAM Identity Center ausvercel loginist 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
- Google
- 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
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.
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-openabfängt und Ports automatisch weiterleitet, um die schlechte User Experience zu kaschieren: https://github.com/phinze/bankshotInteressant. 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:
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 schickthellozurück; wenn der Clienthellonicht erhält, gibt die CLI die URL aus.Wenn der Server gleichzeitig keine Antwort auf das von ihm gesendete
helloerhä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.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.