1 points par GN⁺ 4 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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_uri et interval, puis interroge périodiquement /token tout en gérant des états standard comme authorization_pending, slow_down, access_denied et expired_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 302 de 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
  • gcloud auth login, wrangler login, l’ancien vercel login et 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
  • 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-open ou open
    • 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
  • 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/tcp ou 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 curl manuel de l’URL localhost depuis un second terminal
  • Le claude d’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_endpoint du fournisseur d’identité
    • Une requête d’exemple envoie client_id=my-cli&scope=openid+offline_access
  • Le fournisseur d’identité renvoie un JSON contenant
    • device_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • 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 interval secondes
  • 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’utilisateur
    • slow_down : le fournisseur d’identité demande de ralentir le polling, et la spécification impose d’augmenter l’intervalle d’au moins 5 secondes
    • access_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 /token avant 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 /token est 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_endpoint et token_endpoint depuis .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_endpoint du vrai fournisseur d’identité, obtient un user_code et un device_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 /token avec le device_code qu’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
  • 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.PostForm vers DeviceAuthorizationEndpoint avec client_id et scope
    • décodage du JSON de réponse pour obtenir DeviceCode, UserCode, VerificationURIComplete et Interval
    • affichage de VerificationURIComplete et UserCode à l’utilisateur
    • POST répété vers TokenEndpoint avec device_code, client_id et 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_token et refresh_token
    • toute autre erreur est traitée comme un échec
  • 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
  • interval et slow_down doivent ê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 --web au 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 login l’utilise depuis le départ et est souvent considéré, dans l’open source, comme l’implémentation de référence la plus propre
    • aws sso login exécute le device flow de bout en bout avec IAM Identity Center
    • vercel login a 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
  • 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

 
GN⁺ 4 시간 전
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

    • Cette approche n’aide probablement pas autant que l’article le suggère. Il est assez facile de créer une page de phishing d’atterrissage qui démarre le flux quand l’utilisateur arrive, puis le redirige immédiatement vers le vrai fournisseur
      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-open et fait l’auto-transfert de ports pour masquer cette mauvaise expérience utilisateur : https://github.com/phinze/bankshot

  • Inté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

    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.)
    J’ai donc simplement lu et implémenté le flux RFC 8252. Mon outil est bien un CLI, mais comme le cas d’usage est uniquement local, je n’avais pas pris en compte SSH ni les environnements conteneurisés
    En plus, dans le flux RFC 8268, Google n’autorise que des portées OAuth 2.0 limitées, ce qui peut être une contrainte rédhibitoire pour certaines applications

    • Petite correction : en revérifiant le numéro dans le texte d’origine, c’est RFC 8628
      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-open du 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 renvoyer hello, puis si le client ne reçoit pas hello, le CLI affiche l’URL
    En parallèle, si le serveur ne reçoit pas de réponse au hello qu’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éphone

    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  
    

    L’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

    • Ce flux aussi fonctionne via le navigateur quand tout se passe bien. Il a simplement une meilleure solution de repli quand ça échoue