1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Muchas CLI usan por defecto el redirect de OAuth a localhost, que se completa rápido en el navegador local de una laptop, pero en entornos de desarrollo como SSH, contenedores o WSL esa misma suposición se rompe y el flujo de inicio de sesión se queda atascado
  • El método actual consiste en que la CLI abre un servidor HTTP temporal en 127.0.0.1, envía el navegador a la URL de autenticación y luego el proveedor de autenticación devuelve el authorization code al callback local
  • El RFC 8628 Device Authorization Grant, estandarizado en 2019, separa la CLI que solicita el token del dispositivo con navegador donde el usuario se autentica, eliminando la dependencia de abrir puertos o de tener un navegador local
  • El device flow recibe device_code, user_code, verification_uri e interval, hace polling periódico a /token y maneja estados estándar como authorization_pending, slow_down, access_denied y expired_token
  • En una CLI nueva, el valor por defecto debería ser device flow, descubrir endpoints con .well-known/openid-configuration y guardar el refresh token en el keychain del sistema operativo, no en un archivo JSON dentro de ~/.config

Lo que asume el redirect a localhost

  • Un inicio de sesión típico en una CLI funciona sobre la suposición de que el servidor HTTP local y el navegador del sistema están en la misma máquina
    • La CLI hace bind de un servidor HTTP en un puerto específico de 127.0.0.1
    • Abre el navegador del sistema en el OAuth authorization endpoint e incluye redirect_uri=http://127.0.0.1:<port>/callback
    • Cuando el usuario inicia sesión, el proveedor de autenticación hace un redirect 302 del authorization code a la URL loopback
    • El pequeño servidor HTTP de la CLI lee el code y lo intercambia por tokens en el token endpoint
    • Casi siempre se usa PKCE, y después se muestra una página tipo “puedes cerrar esta pestaña”
  • gcloud auth login, wrangler login, el antiguo vercel login y varias CLI de otros vendors usan este método
    • Wrangler usa el puerto 8976
    • gcloud usa 8085
    • Claude Code toma un puerto temporal cada vez que se ejecuta
  • RFC 8252 recomienda este patrón para native apps cuando hay navegador disponible, pero no cubre qué hacer cuando el host no tiene navegador

Por qué el usuario casi no ve el paso de localhost

  • El callback a localhost ocurre tan rápido que la mayoría de los usuarios ni lo nota
  • La URL que imprime la CLI es larga, y dentro lleva la redirect URI como query string
  • El usuario inicia sesión y da consentimiento en el dominio real del proveedor de autenticación
  • El proveedor manda el navegador al callback local en localhost para que la CLI lea el code, y luego redirige otra vez a una página más pulida de “sesión iniciada”
  • Desde fuera parece “inicié sesión en un sitio web y la CLI quedó autenticada”, pero en realidad el flujo depende de la coexistencia entre un servidor HTTP local y el navegador

Dónde se rompe en SSH, contenedores y WSL

  • Todo el flujo depende de la suposición de que la máquina donde corre la CLI y la máquina donde corre el navegador son la misma
  • En una sesión SSH, el host remoto no tiene navegador, y xdg-open puede fallar o intentar abrir un navegador remoto invisible en un entorno con X forwarding
    • Se puede tunelizar el puerto del callback hacia la laptop, pero la redirect URI registrada en el proveedor de autenticación tiene que permitir el puerto que pasa por el túnel
  • En un contenedor no hay navegador, y muchas imágenes ni siquiera incluyen xdg-open o open
    • Se puede exponer el puerto del callback con -p, pero hace falta saber qué puerto va a elegir la CLI
    • En la CLI de Cloudflare siguen apareciendo issues de usuarios bloqueados por este problema
  • En WSL, el navegador se abre en Windows y el loopback server corre en Linux
    • El port forwarding de WSL2 normalmente funciona, pero no siempre
  • En una máquina compartida, otro proceso en la misma máquina puede encontrar puertos en escucha vía /proc/net/tcp o competir por hacer bind primero a un puerto conocido
    • PKCE protege el code exchange, pero no protege la sesión autenticada del propio redirect

Lo que ya revela el fallback sobre el diseño

  • Las CLI que ofrecen el flujo loopback como predeterminado también traen un fallback para cuando se rompe
  • gcloud tiene --no-launch-browser
  • Wrangler se queda colgado, y el workaround aceptado es hacer curl manualmente a la URL localhost desde una segunda terminal
  • claude de Anthropic imprime “Paste code here if prompted” y se queda esperando
  • En la práctica, estos fallback son una especie de device flow manual, y existen porque el flujo principal no funciona en los entornos donde realmente se usan estas CLI

RFC 8628 Device Authorization Grant

  • RFC 8628 es el OAuth 2.0 Device Authorization Grant publicado en 2019 para “input-constrained devices”
    • Incluye como objetivo TVs, consolas y CLI
    • La idea central es separar el dispositivo que solicita el token del dispositivo donde el usuario se autentica
  • La CLI hace un POST al device_authorization_endpoint del proveedor de autenticación
    • Un request de ejemplo envía client_id=my-cli&scope=openid+offline_access
  • El proveedor devuelve un JSON con los siguientes valores
    • device_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • La CLI imprime la URL y un código corto, y si puede también muestra un QR para verification_uri_complete
  • El usuario abre la URL en el dispositivo que quiera, inicia sesión, revisa el scope solicitado y el nombre del cliente, confirma que coincide con el código corto mostrado por la CLI y aprueba

Polling y manejo de estados estándar

  • La CLI hace polling al token endpoint cada interval segundos
  • El grant type usado es urn:ietf:params:oauth:grant-type:device_code
  • RFC 8628 section 3.5 define estos estados
    • authorization_pending: está esperando la aprobación del usuario
    • slow_down: el proveedor pide espaciar más el polling, y la especificación indica aumentar el interval por al menos 5 segundos
    • access_denied: el usuario rechazó la solicitud
    • expired_token: pasó demasiado tiempo y el token expiró
  • En device flow, la CLI no hace bind de puertos ni asume que el host de ejecución tenga navegador
  • El mismo inicio de sesión funciona en una laptop, en un contenedor o en un job de CI que espera aprobación humana

Costo del polling y descubrimiento de endpoints

  • El interval de polling por defecto es de 5 segundos
  • La mayoría de las autenticaciones terminan en menos de 1 minuto, así que un login normal hace alrededor de 10 polls a /token y se detiene
  • El servidor puede aumentar el interval con slow_down, y un cliente bien hecho debe respetarlo
  • Comparado con mantener una conexión WebSocket o SSE a un endpoint con estado por cada login pendiente, el polling stateless a /token es más simple y más barato
  • Si el proveedor de autenticación soporta OpenID Connect Discovery, la CLI puede obtener device_authorization_endpoint y token_endpoint desde .well-known/openid-configuration y evitar hardcodear URLs

El riesgo de phishing en device flow

  • En device flow existe un ataque donde un atacante llama al device_authorization_endpoint del proveedor real, obtiene user_code y device_code, y luego convence a la víctima de ingresarlo
  • La víctima puede iniciar sesión en la URL real, con el código real, y aprobar la pantalla de consentimiento real
  • El atacante hace polling a /token con el device_code que él generó y termina recibiendo el access token
  • Un threat actor ruso llevó a cabo esta campaña contra tenants de M365 desde agosto de 2024
    • Microsoft Threat Intelligence la rastrea como Storm-2372
    • Volexity la atribuye a APT29/Midnight Blizzard
    • Tenants de gobierno, defensa y ONG fueron afectados en varios continentes

La defensa contra phishing es responsabilidad del proveedor de autenticación

  • La defensa contra phishing debe hacerse del lado del proveedor de autenticación, no de la CLI
  • Las mitigaciones necesarias incluyen
    • tiempos de expiración cortos para user_code
    • mostrar de forma visible el nombre del cliente y el origen de la solicitud en la página de verificación
    • rate limiting para los intentos de ingresar códigos
    • no exponer verification_uri_complete, para que la víctima tenga que escribir el código manualmente en vez de hacer clic en un link
    • en tenants de alto valor, usar políticas de acceso condicional para bloquear device code flow si no proviene de una red o dispositivo conocido
  • El papel de la CLI es seguir la especificación y no crear atajos inseguros
  • Device flow cambia una superficie de ataque local por una superficie de ataque social, pero ofrece un flujo que funciona en más entornos y puede aprovechar las mitigaciones del proveedor de autenticación

Flujo clave de una implementación de ejemplo en Go

  • La implementación completa cabe en unas 30 líneas usando solo net/http de Go
  • El flujo de implementación es así
    • llamar a http.PostForm sobre DeviceAuthorizationEndpoint con client_id y scope
    • decodificar del JSON de respuesta DeviceCode, UserCode, VerificationURIComplete e Interval
    • imprimir VerificationURIComplete y UserCode al usuario
    • hacer POST repetido al TokenEndpoint con device_code, client_id y el device grant type
    • si aparece authorization_pending, seguir esperando
    • si aparece slow_down, aumentar el interval en 5 segundos
    • si no hay error, devolver access_token y refresh_token
    • cualquier otro error se trata como fallo
  • Si activas la capacidad “OAuth 2.0 Device Authorization Grant” en un realm de Keycloak, o usas un proveedor certificado por OpenID que soporte este grant, el login por device flow funciona

Qué debería ser el valor por defecto en una CLI nueva

  • El valor por defecto debería ser device flow
  • Los endpoints deben descubrirse desde .well-known/openid-configuration, sin hardcodear URLs
  • interval y slow_down deben respetarse obligatoriamente
  • El refresh token debe guardarse en el keychain del sistema operativo, no en un archivo JSON dentro de ~/.config
  • Si quieres ofrecer una ruta loopback para logins rápidos en laptop, debería ir detrás de un flag --web, no como comportamiento predeterminado

CLI que ya migraron y herramientas que siguen pendientes

  • Hay CLI que ya usan device flow por defecto
    • gh auth login usa device flow desde el inicio, y se le considera una de las implementaciones de referencia open source más limpias
    • aws sso login ejecuta device flow end-to-end contra IAM Identity Center
    • vercel login migró a RFC 8628 en septiembre de 2025, reemplazando el login por email y el antiguo flag --oob
    • Stripe CLI no usa exactamente RFC 8628, pero sí un pairing-code flow con una UX bien resuelta
  • También siguen existiendo herramientas que mantienen loopback flow como predeterminado y le agregan un fallback de pegar el código
    • Google gcloud
    • Cloudflare wrangler
    • Anthropic claude
  • Si una CLI necesita un fallback manual de pegar el código cada vez que sale de la laptop, entonces ese fallback debería ser el flujo principal

1 comentarios

 
GN⁺ 4 시간 전
Comentarios en Lobste.rs
  • La redacción es algo descuidada, pero es interesante. Si el código/enlace del dispositivo se reemplaza cada minuto, quizá también se reduzca su uso en phishing
    Después de usarse una vez, bastaría con detener la rotación y vincular esa sesión a la IP o al navegador

    • Este método no ayuda tanto como dice el artículo. Es bastante fácil crear una página de aterrizaje de phishing que, cuando entra el usuario, inicie el flujo y redirija de inmediato al proveedor legítimo
      Si el proveedor hace que el usuario escriba el código manualmente, como Microsoft, la página de aterrizaje también podría mostrar instrucciones y copiar el código al portapapeles para facilitar aún más el phishing
  • Buen artículo, y coincido en que todos deberían pasarse a RFC 8628
    Como he pasado demasiadas veces por el flujo de OAuth de CLI en máquinas remotas de desarrollo, hice una herramienta personal que intercepta xdg-open y reenvía puertos automáticamente para disimular la mala experiencia de usuario: https://github.com/phinze/bankshot

  • Interesante. Justo hace poco implementé el método de autenticación “viejo”, RFC 8252, y no conocía el método “nuevo”, RFC 8268
    Supongo que esa laguna de conocimiento se debe a que mi caso de uso principal era la autenticación con servidores de Google. La documentación que yo creía que correspondía al flujo RFC 8268 dice explícitamente lo siguiente

    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.)
    Así que me limité a leer e implementar solo el flujo RFC 8252. Mi herramienta sí es una CLI, pero como el caso de uso era exclusivamente local, no tomé en cuenta entornos con SSH o contenedores
    Además, en el flujo RFC 8268 Google solo permite alcances limitados de OAuth 2.0, lo que para algunas aplicaciones puede ser una limitación decisiva

    • Corrección menor: revisé de nuevo el número del original y en realidad es RFC 8628
      La restricción de alcances de Google es la parte donde OIDC asoma su complejidad. Idealmente, Google debería devolver un token de ID en vez de meter todo a la fuerza dentro del token de acceso, pero eso es un problema de la configuración OAuth de Google, no una característica propia de 8628
      De ahí viene la complejidad interminable de OAuth. El estándar define bien el marco de cómo construir y transportar un esquema de autorización, pero deliberadamente no dice nada sobre qué debería ser. Incluso para obtener el conjunto común de endpoints HTTP en el que están de acuerdo “la mayoría” de los proveedores, hicieron falta la invención de OIDC y varios años más
  • Otro truco es reenviar al laptop la llamada a xdg-open del servidor. Hice una herramienta pequeña para mi infraestructura personal que hace eso: https://github.com/zimbatm/subportal/

  • ¿No se podrían combinar ambos enfoques? La idea sería redirigir a una URL de localhost y hacer que devuelva hello; si el cliente no recibe hello, entonces la CLI muestra la URL
    Al mismo tiempo, si el servidor no recibe respuesta al hello que envió, puede mostrar un código en el navegador y un mensaje como “confirma si estás intentando iniciar sesión”. También podría hacerse más fácil mostrando números para elegir desde el teléfono, como hace Google

    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  
    

    La ventaja es que, incluso en el caso 2, a la gente le resulta fácil hacer clic en un enlace, pero compartir OTP/códigos relativamente menos, y además el atacante tendría que seguir interviniendo con ingeniería social durante todo el ataque

  • Cuando funciona bien en una máquina local, no hace falta interacción, así que me gustaría que el valor predeterminado fuera el flujo basado en navegador

    • Este flujo también funciona con navegador cuando todo sale bien. La diferencia es que tiene una mejor ruta alternativa cuando falla