Authentification CLI : la bonne approche
(abgeo.dev)- De nombreuses CLI utilisent par défaut une redirection OAuth localhost rapide dans le navigateur local d’un laptop, mais cette hypothèse casse dans des environnements de développement comme SSH, les conteneurs ou WSL, et le flux de connexion se bloque
- Le fonctionnement actuel consiste à faire ouvrir par la CLI un serveur HTTP temporaire sur
127.0.0.1, à envoyer le navigateur vers l’URL d’authentification, puis à laisser le fournisseur d’identité renvoyer l’authorization code vers un callback local - Le RFC 8628 Device Authorization Grant, standardisé en 2019, sépare la CLI qui demande le token et l’appareil navigateur sur lequel l’utilisateur s’authentifie, supprimant la dépendance au bind de port et au navigateur local
- Le device flow reçoit
device_code,user_code,verification_urietinterval, puis interroge périodiquement/tokentout en gérant des états standard commeauthorization_pending,slow_down,access_deniedetexpired_token - Pour une nouvelle CLI, le device flow devrait être le comportement par défaut, les endpoints devraient être découverts via
.well-known/openid-configuration, et les refresh tokens devraient être stockés dans le trousseau système plutôt que dans un fichier JSON sous~/.config
Ce que suppose la redirection localhost
- La connexion CLI la plus courante fonctionne en supposant qu’un serveur HTTP local et le navigateur système se trouvent sur la même machine
- La CLI bind un serveur HTTP sur un port donné de
127.0.0.1 - Elle ouvre le navigateur système sur l’endpoint OAuth authorization avec
redirect_uri=http://127.0.0.1:<port>/callback - Une fois l’utilisateur connecté, le fournisseur d’identité effectue une redirection
302de l’authorization code vers l’URL loopback - Le petit serveur HTTP de la CLI lit le code et l’échange contre un token auprès du token endpoint
- La plupart du temps, PKCE est utilisé, puis une page du type « vous pouvez fermer cet onglet » s’affiche
- La CLI bind un serveur HTTP sur un port donné de
gcloud auth login,wrangler login, l’ancienvercel loginet plusieurs CLI de vendors utilisent cette méthode- Wrangler utilise le port
8976 - gcloud utilise
8085 - Claude Code choisit un port temporaire à chaque exécution
- Wrangler utilise le port
- Le RFC 8252 recommande ce pattern pour les applications natives lorsqu’un navigateur est disponible, mais ne traite pas le cas où l’hôte n’en a pas
Pourquoi les utilisateurs voient mal l’étape localhost
- Le callback localhost passe très vite, donc la plupart des utilisateurs ne le voient pas
- L’URL affichée par la CLI est longue, et la redirect URI y apparaît dans la query string
- L’utilisateur se connecte et donne son consentement sur le vrai domaine du fournisseur d’identité
- Le fournisseur d’identité renvoie ensuite le navigateur vers le callback localhost, la CLI lit le code, puis l’utilisateur est redirigé vers une page de confirmation soignée du type « signed in »
- En apparence, on a juste « je me suis connecté sur un site web et la CLI a été authentifiée », mais en réalité, c’est la cohabitation entre le navigateur et un serveur HTTP local qui fait tenir tout le flux
Là où ça casse avec SSH, les conteneurs et WSL
- L’ensemble du flux repose sur l’hypothèse que la machine qui exécute la CLI est la même que celle qui exécute le navigateur
- Dans une session SSH, l’hôte distant n’a pas de navigateur, et
xdg-openéchoue ou peut ouvrir un navigateur distant invisible dans un environnement avec X forwarding- Il est possible de tunneliser le port de callback vers le laptop, mais encore faut-il que la redirect URI enregistrée auprès du fournisseur d’identité autorise ce port tunnelisé
- Un conteneur n’a pas de navigateur, et de nombreuses images n’incluent même pas
xdg-openouopen- On peut exposer le port de callback avec
-p, mais il faut savoir quel port la CLI va choisir - La CLI de Cloudflare accumule les issues d’utilisateurs bloqués par ce problème
- On peut exposer le port de callback avec
- Dans WSL, le navigateur s’ouvre côté Windows alors que le serveur loopback tourne sous Linux
- Le port forwarding de WSL2 fonctionne souvent, mais pas systématiquement
- Sur une machine partagée, un autre processus peut repérer un port à l’écoute via
/proc/net/tcpou tenter de binder en premier sur un port connu- PKCE protège l’échange du code, mais pas la session authentifiée qui sous-tend la redirection elle-même
Le fallback révèle déjà un problème de conception
- Les CLI qui proposent le flux loopback par défaut fournissent aussi souvent un fallback quand il casse
- gcloud dispose de
--no-launch-browser - Wrangler se bloque, et le contournement accepté consiste à faire un
curlmanuel de l’URL localhost depuis un second terminal - Le
clauded’Anthropic affiche « Paste code here if prompted » puis attend - Ces fallbacks sont en pratique un device flow manuel : s’ils existent, c’est bien parce que le flux par défaut ne fonctionne pas dans les environnements où la CLI est réellement utilisée
RFC 8628 Device Authorization Grant
- Le RFC 8628, publié en 2019, définit l’OAuth 2.0 Device Authorization Grant pour les « input-constrained devices »
- Cela inclut les TV, les consoles et les CLI
- L’idée centrale est de séparer l’appareil qui demande le token de celui sur lequel l’utilisateur s’authentifie
- La CLI fait un POST vers le
device_authorization_endpointdu fournisseur d’identité- Une requête d’exemple envoie
client_id=my-cli&scope=openid+offline_access
- Une requête d’exemple envoie
- Le fournisseur d’identité renvoie un JSON contenant
device_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
- La CLI affiche l’URL et un code court, et si possible montre aussi un QR code vers
verification_uri_complete - L’utilisateur ouvre l’URL depuis l’appareil de son choix, se connecte, consulte les scopes demandés et le nom du client, vérifie que cela correspond bien au code court affiché dans la CLI, puis approuve
Polling et gestion standard des états
- La CLI interroge le token endpoint toutes les
intervalsecondes - Le grant type utilisé est
urn:ietf:params:oauth:grant-type:device_code - La section 3.5 du RFC 8628 définit les états suivants
authorization_pending: en attente de l’approbation de l’utilisateurslow_down: le fournisseur d’identité demande de ralentir le polling, et la spécification impose d’augmenter l’intervalle d’au moins 5 secondesaccess_denied: l’utilisateur a refuséexpired_token: le token a expiré après une attente trop longue
- Avec le device flow, la CLI ne bind aucun port et ne suppose pas qu’un navigateur existe sur l’hôte d’exécution
- La même méthode de connexion fonctionne sur un laptop, dans un conteneur ou dans un job CI en attente d’une validation humaine
Coût du polling et découverte des endpoints
- L’intervalle de polling par défaut est de 5 secondes
- La plupart des authentifications se terminent en moins d’une minute, donc une connexion classique effectue une dizaine d’interrogations de
/tokenavant de s’arrêter - Le serveur peut augmenter l’intervalle via
slow_down, et un client bien implémenté doit le respecter - Comparé au maintien d’une connexion WebSocket ou SSE vers un endpoint stateful pour chaque connexion en attente, un polling stateless sur
/tokenest plus simple et moins coûteux - Si le fournisseur d’identité prend en charge OpenID Connect Discovery, la CLI peut récupérer
device_authorization_endpointettoken_endpointdepuis.well-known/openid-configuration, sans hardcoder les URL
Le risque de phishing du device flow
- Le device flow permet une attaque où un attaquant appelle le
device_authorization_endpointdu vrai fournisseur d’identité, obtient unuser_codeet undevice_code, puis pousse la victime à les utiliser - La victime peut alors se connecter sur la vraie URL, avec le vrai code, et approuver le vrai écran de consentement
- Pendant ce temps, l’attaquant poll
/tokenavec ledevice_codequ’il a généré et récupère l’access token - Des acteurs malveillants russes mènent cette campagne contre des tenants M365 depuis août 2024
- Microsoft Threat Intelligence suit cette activité sous le nom Storm-2372
- Volexity l’attribue à APT29/Midnight Blizzard
- Des tenants gouvernementaux, de défense et d’ONG ont été touchés sur plusieurs continents
La défense contre le phishing relève du fournisseur d’identité
- La défense contre le phishing doit être mise en place côté fournisseur d’identité, pas côté CLI
- Les mitigations nécessaires sont les suivantes
- une durée d’expiration courte pour le
user_code - un affichage très visible du nom du client et de l’origine de la demande sur la page de vérification
- du rate limiting sur les tentatives de saisie de code
- l’absence d’exposition de
verification_uri_complete, afin que la victime saisisse elle-même le code au lieu de cliquer sur un lien - pour les tenants à forte valeur, des politiques de conditional access bloquant le device code flow hors des réseaux ou appareils connus
- une durée d’expiration courte pour le
- Le rôle de la CLI est de suivre la spécification et de ne pas créer de raccourcis dangereux
- Le device flow remplace une surface d’attaque locale par une surface d’attaque sociale, mais il fournit un flux qui fonctionne dans davantage d’environnements et permet de bénéficier des mitigations du fournisseur d’identité
L’essentiel d’une implémentation Go
- Une implémentation complète tient en Go en une trentaine de lignes avec
net/http - Le flux d’implémentation est le suivant
- appel à
http.PostFormversDeviceAuthorizationEndpointavecclient_idetscope - décodage du JSON de réponse pour obtenir
DeviceCode,UserCode,VerificationURICompleteetInterval - affichage de
VerificationURICompleteetUserCodeà l’utilisateur - POST répété vers
TokenEndpointavecdevice_code,client_idet le device grant type - si
authorization_pending, on continue d’attendre - si
slow_down, on augmente l’intervalle de 5 secondes - s’il n’y a pas d’erreur, on renvoie
access_tokenetrefresh_token - toute autre erreur est traitée comme un échec
- appel à
- En activant la capability « OAuth 2.0 Device Authorization Grant » dans un realm Keycloak, ou en utilisant un provider certifié OpenID qui prend en charge ce grant, la connexion en device flow fonctionne
Ce qu’il faut choisir comme valeur par défaut pour une nouvelle CLI
- Le device flow doit être le comportement par défaut
- Les endpoints doivent être découverts via
.well-known/openid-configuration, sans hardcoder les URL intervaletslow_downdoivent être strictement respectés- Le refresh token doit être stocké dans le trousseau système de l’OS, pas dans un fichier JSON sous
~/.config - Si vous voulez proposer un chemin loopback pour une connexion rapide sur laptop, placez-le derrière un flag
--webau lieu d’en faire le défaut
Les CLI qui ont déjà migré et celles qui traînent encore
- Certaines CLI utilisent déjà le device flow par défaut
gh auth loginl’utilise depuis le départ et est souvent considéré, dans l’open source, comme l’implémentation de référence la plus propreaws sso loginexécute le device flow de bout en bout avec IAM Identity Centervercel logina migré vers le RFC 8628 en septembre 2025, remplaçant la connexion par email et l’ancien flag--oob- La Stripe CLI n’implémente pas exactement le RFC 8628, mais propose une bonne UX avec un pairing-code flow
- D’autres outils gardent encore le loopback flow par défaut avec un fallback de type paste-the-code
- Google
gcloud - Cloudflare
wrangler - Anthropic
claude
- Google
- Si une CLI a besoin d’un fallback manuel paste-the-code dès qu’elle sort du laptop, alors ce fallback devrait devenir le flux par défaut
1 commentaires
Avis sur Lobste.rs
C’est formulé un peu à la va-vite, mais c’est intéressant. Remplacer les codes/liens d’appareil toutes les minutes pourrait aussi réduire leur exploitation pour le phishing
Il suffirait d’arrêter la rotation après la première utilisation, puis d’associer cette session à une IP ou à un navigateur
Pour les fournisseurs comme Microsoft, où l’utilisateur doit saisir lui-même le code, la page d’atterrissage pourrait afficher des instructions et copier le code dans le presse-papiers pour rendre le phishing encore plus efficace
Bon article, et je suis d’accord sur le fait que tout le monde devrait migrer vers RFC 8628
À force de subir trop souvent les procédures OAuth CLI sur des machines de développement distantes, j’ai créé un outil perso qui intercepte
xdg-openet fait l’auto-transfert de ports pour masquer cette mauvaise expérience utilisateur : https://github.com/phinze/bankshotIntéressant. J’ai justement implémenté récemment l’« ancien » mode d’authentification, RFC 8252, sans connaître le « nouveau », RFC 8268
Comme mon principal cas d’usage concernait l’authentification aux serveurs Google, j’ai sans doute eu un angle mort. Dans la documentation que je pensais décrire le flux RFC 8268, il est indiqué ceci
La restriction de portée chez Google est là où OIDC refait surface de façon compliquée. Idéalement, Google devrait renvoyer un jeton d’identité au lieu de tout écraser dans le jeton d’accès, mais c’est un problème de configuration OAuth chez Google, pas une caractéristique propre à 8628
La complexité sans fin d’OAuth vient de là. Le standard définit bien le cadre de comment construire et transporter un schéma d’autorisation, mais reste volontairement silencieux sur ce qu’il doit être. Il a même fallu l’invention d’OIDC et plusieurs années pour obtenir l’ensemble commun de points de terminaison HTTP sur lequel « la plupart » des fournisseurs s’accordent
Un autre hack consiste à rediriger vers son laptop l’appel
xdg-opendu serveur. J’ai créé un petit outil qui fait ça pour mon infra perso : https://github.com/zimbatm/subportal/On ne pourrait pas combiner les deux approches ? On redirige vers une URL
localhost, on lui fait renvoyerhello, puis si le client ne reçoit pashello, le CLI affiche l’URLEn parallèle, si le serveur ne reçoit pas de réponse au
helloqu’il a envoyé, il peut afficher un code dans le navigateur avec un message du type « vérifiez que vous êtes bien en train d’essayer de vous connecter ». On pourrait aussi rendre ça plus simple, comme Google qui affiche un nombre à choisir sur le téléphoneL’avantage, c’est que même dans le cas 2, les gens cliquent facilement sur un lien mais partagent beaucoup moins volontiers un OTP/un code, et que l’attaquant doit continuer à intervenir par ingénierie sociale pendant toute l’attaque
Quand ça fonctionne bien sur une machine locale, il n’y a pas besoin d’interaction, donc le flux basé sur le navigateur devrait être la valeur par défaut