Autenticación de CLI: la forma correcta
(abgeo.dev)- 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_urieinterval, hace polling periódico a/tokeny maneja estados estándar comoauthorization_pending,slow_down,access_deniedyexpired_token - En una CLI nueva, el valor por defecto debería ser device flow, descubrir endpoints con
.well-known/openid-configurationy 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
302del 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”
- La CLI hace bind de un servidor HTTP en un puerto específico de
gcloud auth login,wrangler login, el antiguovercel loginy 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
- Wrangler usa el puerto
- 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-openpuede 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-openoopen- 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
- Se puede exponer el puerto del callback con
- 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/tcpo 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
curlmanualmente a la URL localhost desde una segunda terminal claudede 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_endpointdel proveedor de autenticación- Un request de ejemplo envía
client_id=my-cli&scope=openid+offline_access
- Un request de ejemplo envía
- El proveedor devuelve un JSON con los siguientes valores
device_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
- 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
intervalsegundos - 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 usuarioslow_down: el proveedor pide espaciar más el polling, y la especificación indica aumentar el interval por al menos 5 segundosaccess_denied: el usuario rechazó la solicitudexpired_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
/tokeny 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
/tokenes más simple y más barato - Si el proveedor de autenticación soporta OpenID Connect Discovery, la CLI puede obtener
device_authorization_endpointytoken_endpointdesde.well-known/openid-configurationy evitar hardcodear URLs
El riesgo de phishing en device flow
- En device flow existe un ataque donde un atacante llama al
device_authorization_endpointdel proveedor real, obtieneuser_codeydevice_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
/tokencon eldevice_codeque é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
- tiempos de expiración cortos para
- 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/httpde Go - El flujo de implementación es así
- llamar a
http.PostFormsobreDeviceAuthorizationEndpointconclient_idyscope - decodificar del JSON de respuesta
DeviceCode,UserCode,VerificationURICompleteeInterval - imprimir
VerificationURICompleteyUserCodeal usuario - hacer POST repetido al
TokenEndpointcondevice_code,client_idy 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_tokenyrefresh_token - cualquier otro error se trata como fallo
- llamar a
- 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 intervalyslow_downdeben 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 loginusa device flow desde el inicio, y se le considera una de las implementaciones de referencia open source más limpiasaws sso loginejecuta device flow end-to-end contra IAM Identity Centervercel loginmigró 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
- Google
- 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
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
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-openy reenvía puertos automáticamente para disimular la mala experiencia de usuario: https://github.com/phinze/bankshotInteresante. 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
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-opendel 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
localhosty hacer que devuelvahello; si el cliente no recibehello, entonces la CLI muestra la URLAl mismo tiempo, si el servidor no recibe respuesta al
helloque 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 GoogleLa 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