1 pontos por GN⁺ 4 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • Muitas CLIs usam por padrão o redirecionamento OAuth para localhost, que funciona rápido no navegador local de um notebook, mas essa mesma suposição quebra em ambientes de desenvolvimento como SSH, contêineres e WSL, fazendo o fluxo de login travar
  • No modelo atual, a CLI abre um servidor HTTP temporário em 127.0.0.1, envia o navegador para a URL de autenticação e, depois, o provedor de autenticação devolve o authorization code para um callback local
  • O RFC 8628 Device Authorization Grant, padronizado em 2019, separa a CLI que solicita o token do dispositivo com navegador usado pelo usuário para autenticar, eliminando a dependência de bind de porta ou de navegador local
  • No device flow, a CLI recebe device_code, user_code, verification_uri e interval, faz polling periódico em /token e trata estados padrão como authorization_pending, slow_down, access_denied e expired_token
  • Em uma nova CLI, o padrão deveria ser device flow, com descoberta de endpoints via .well-known/openid-configuration, e o refresh token deveria ser armazenado no keychain do sistema operacional, não em um arquivo JSON em ~/.config

O que o redirecionamento para localhost pressupõe

  • Um login de CLI comum funciona assumindo que o servidor HTTP local e o navegador do sistema estão na mesma máquina
    • A CLI faz bind de um servidor HTTP em uma porta específica de 127.0.0.1
    • Abre o navegador do sistema no endpoint de autorização OAuth, incluindo redirect_uri=http://127.0.0.1:<port>/callback
    • Quando o usuário faz login, o provedor de autenticação redireciona com 302 o authorization code para a URL de loopback
    • O pequeno servidor HTTP da CLI lê o code e o troca por tokens no token endpoint
    • Na maioria dos casos isso vem com PKCE, e depois aparece uma página do tipo “você já pode fechar esta aba”
  • gcloud auth login, wrangler login, versões antigas de vercel login e várias CLIs de fornecedores usam esse modelo
    • O Wrangler usa a porta 8976
    • O gcloud usa 8085
    • O Claude Code escolhe uma porta temporária a cada execução
  • O RFC 8252 recomenda esse padrão para apps nativos quando há navegador disponível, mas não trata do caso em que não existe navegador no host

Por que os usuários quase não veem a etapa de localhost

  • O callback para localhost passa muito rápido, então a maioria dos usuários nem chega a vê-lo
  • A URL exibida pela CLI é longa e inclui a redirect URI dentro da query string
  • O usuário faz login e concede acesso no domínio real do provedor de autenticação
  • O provedor então manda o navegador para o callback em localhost, a CLI lê o code e em seguida o navegador vai para uma página polida de “login concluído”
  • Na aparência, parece apenas que “fiz login no site e a CLI foi autenticada”, mas na prática o fluxo depende da coexistência entre o navegador e um servidor HTTP local

Onde isso quebra em SSH, contêineres e WSL

  • Todo o fluxo depende da suposição de que a máquina onde a CLI roda é a mesma onde o navegador roda
  • Em uma sessão SSH, o host remoto não tem navegador, e xdg-open pode falhar ou abrir um navegador remoto invisível em um ambiente com X forwarding
    • Dá para tunelar a porta de callback até o notebook, mas a redirect URI registrada no provedor precisa permitir a porta que atravessa o túnel
  • Contêineres não têm navegador, e muitas imagens nem incluem xdg-open ou open
    • É possível expor a porta de callback com -p, mas é preciso saber qual porta a CLI vai usar
    • A CLI da Cloudflare acumula issues de usuários bloqueados por esse problema
  • No WSL, o navegador abre no Windows, enquanto o servidor de loopback roda no Linux
    • O encaminhamento de portas do WSL2 costuma funcionar, mas nem sempre
  • Em máquinas compartilhadas, outro processo na mesma máquina pode descobrir portas em escuta via /proc/net/tcp ou disputar o bind antecipado de portas conhecidas
    • O PKCE protege a troca do code, mas não protege a própria sessão autenticada do redirecionamento

O fallback já expõe o problema de design

  • CLIs que oferecem o fluxo de loopback como padrão normalmente também trazem um fallback para quando ele quebra
  • O gcloud tem --no-launch-browser
  • O Wrangler trava, e o workaround aceito é dar curl manualmente na URL de localhost a partir de um segundo terminal
  • O claude da Anthropic exibe “Paste code here if prompted” e fica aguardando
  • Esses fallbacks são, na prática, um device flow manual, e existem porque o fluxo padrão não funciona nos ambientes em que a CLI realmente é usada

RFC 8628 Device Authorization Grant

  • O RFC 8628 é o OAuth 2.0 Device Authorization Grant, padronizado em 2019 para “input-constrained devices”
    • Isso inclui TVs, consoles e CLIs
    • O ponto central é separar o dispositivo que solicita o token do dispositivo em que o usuário autentica
  • A CLI faz um POST para o device_authorization_endpoint do provedor de autenticação
    • Um exemplo de requisição envia client_id=my-cli&scope=openid+offline_access
  • O provedor responde com um JSON contendo
    • device_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • A CLI exibe a URL e um código curto e, se possível, também mostra um QR para verification_uri_complete
  • O usuário abre a URL no dispositivo que quiser, faz login, vê o scope solicitado e o nome do cliente, confere se bate com o código curto mostrado na CLI e então aprova

Polling e tratamento padronizado de estados

  • A CLI faz polling no token endpoint a cada interval segundos
  • O grant type usado é urn:ietf:params:oauth:grant-type:device_code
  • A seção 3.5 do RFC 8628 define os seguintes estados
    • authorization_pending: aguardando a aprovação do usuário
    • slow_down: o provedor pede para reduzir a frequência do polling, e a especificação exige aumentar o interval em pelo menos 5 segundos
    • access_denied: o usuário recusou
    • expired_token: o token expirou por demora excessiva
  • No device flow, a CLI não faz bind de porta nem assume que há um navegador no host de execução
  • O mesmo login funciona em notebook, contêiner ou job de CI esperando aprovação humana

Custo do polling e descoberta de endpoints

  • O interval padrão de polling é de 5 segundos
  • Como a maioria das autenticações termina em menos de 1 minuto, um login normal costuma fazer cerca de 10 polls em /token e parar
  • O servidor pode aumentar o interval com slow_down, e um cliente bem implementado precisa obedecer
  • Comparado a manter uma conexão WebSocket ou SSE em um endpoint stateful para cada login pendente, o polling stateless em /token é mais simples e mais barato
  • Se o provedor suportar OpenID Connect Discovery, a CLI pode obter device_authorization_endpoint e token_endpoint de .well-known/openid-configuration, evitando URLs hardcoded

O risco de phishing no device flow

  • No device flow, existe um ataque em que o invasor chama o device_authorization_endpoint do provedor real, recebe user_code e device_code e induz a vítima a inserir o código
  • A vítima pode fazer login na URL real, com o código real, e aprovar a tela de consentimento real
  • O invasor fica fazendo polling em /token com o device_code que ele próprio gerou e recebe o access token
  • Um threat actor russo conduz essa campanha contra tenants do M365 desde agosto de 2024
    • A Microsoft Threat Intelligence rastreia isso como Storm-2372
    • A Volexity atribui a APT29/Midnight Blizzard
    • Tenants de governo, defesa e ONGs foram afetados em vários continentes

A defesa contra phishing é responsabilidade do provedor

  • A defesa contra phishing deve ser feita do lado do provedor de autenticação, não da CLI
  • As mitigações necessárias incluem
    • tempo curto de expiração do user_code
    • destaque visível do nome do cliente e da origem da solicitação na página de verificação
    • rate limiting nas tentativas de inserir códigos
    • não expor verification_uri_complete, para que a vítima digite o código manualmente em vez de apenas clicar no link
    • em tenants de alto valor, bloquear o device code flow com políticas de acesso condicional quando a origem não vier de rede ou dispositivo conhecido
  • O papel da CLI é seguir a especificação e não criar atalhos
  • O device flow troca uma superfície de ataque local por uma superfície de ataque social, mas faz mais sentido oferecer um fluxo que funciona em mais ambientes e aproveitar as mitigações do provedor

O fluxo principal da implementação de exemplo em Go

  • A implementação completa cabe em cerca de 30 linhas em Go usando apenas net/http
  • O fluxo de implementação é o seguinte
    • chamar http.PostForm no DeviceAuthorizationEndpoint com client_id e scope
    • decodificar do JSON de resposta DeviceCode, UserCode, VerificationURIComplete e Interval
    • exibir VerificationURIComplete e UserCode ao usuário
    • fazer POST repetidamente no TokenEndpoint com device_code, client_id e o device grant type
    • se houver authorization_pending, continuar aguardando
    • se houver slow_down, aumentar o interval em 5 segundos
    • se não houver erro, retornar access_token e refresh_token
    • qualquer outro erro deve ser tratado como falha
  • Se você ativar a capability “OAuth 2.0 Device Authorization Grant” em um realm do Keycloak ou usar um provedor OpenID certificado que suporte esse grant, o login por device flow funciona

O que deveria ser o padrão em novas CLIs

  • O padrão deve ser device flow
  • Os endpoints devem ser descobertos via .well-known/openid-configuration, sem hardcode de URLs
  • interval e slow_down precisam ser respeitados obrigatoriamente
  • O refresh token deve ser armazenado no keychain do sistema operacional, e não em um arquivo JSON dentro de ~/.config
  • Se você quiser oferecer um caminho por loopback para login rápido em notebook, ele deve ficar atrás de uma flag --web, e não como padrão

CLIs que já migraram e ferramentas que ainda faltam

  • Há CLIs que já usam device flow por padrão
    • gh auth login usa device flow desde o começo e é considerado uma das implementações de referência mais limpas em open source
    • aws sso login executa o device flow de ponta a ponta com o IAM Identity Center
    • vercel login migrou para o RFC 8628 em setembro de 2025, substituindo o login por email e a antiga flag --oob
    • A Stripe CLI não usa exatamente o RFC 8628, mas oferece uma boa UX com um pairing-code flow
  • Ainda há ferramentas que mantêm o loopback flow como padrão e só acrescentam um fallback de colar código
    • Google gcloud
    • Cloudflare wrangler
    • Anthropic claude
  • Se uma CLI precisa de fallback manual de colar código toda vez que sai do notebook, então esse fallback deveria ser o fluxo padrão

1 comentários

 
GN⁺ 4 시간 전
Comentários do Lobste.rs
  • A formulação é meio solta, mas é interessante. Trocar o código/link do dispositivo a cada 1 minuto também poderia reduzir o abuso em phishing
    Depois de usado uma vez, bastaria parar a rotação e vincular aquela sessão ao IP ou navegador

    • Isso não ajuda tanto quanto o texto sugere. É bem fácil criar uma página de phishing que inicia o fluxo quando o usuário entra e imediatamente redireciona para o provedor legítimo
      Se for um provedor como a Microsoft, em que o usuário precisa digitar o código manualmente, a página de entrada pode exibir instruções e copiar o código para a área de transferência, facilitando ainda mais o phishing
  • Bom texto, e concordo que todo mundo deveria migrar para a RFC 8628
    Como passei por fluxos de OAuth de CLI em máquinas remotas de desenvolvimento vezes demais, criei uma ferramenta pessoal que intercepta xdg-open e faz encaminhamento automático de portas para mascarar a experiência ruim: https://github.com/phinze/bankshot

  • Interessante. Recentemente implementei o modo de autenticação “antigo”, a RFC 8252, e não conhecia o modo “novo”, a RFC 8628
    Acho que essa lacuna veio do fato de meu principal caso de uso ser autenticação com servidores do Google. Na documentação que eu pensava ser sobre o fluxo da RFC 8628, está escrito o seguinte

    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.)
    Então acabei lendo e implementando apenas o fluxo da RFC 8252. Minha ferramenta realmente é uma CLI, mas como o caso de uso é só local, não considerei ambientes com SSH ou contêineres
    Além disso, no fluxo da RFC 8628 o Google só permite escopos OAuth 2.0 limitados, o que pode ser uma limitação decisiva para algumas aplicações

    • Pequena correção: conferi de novo a numeração no texto original e é RFC 8628
      A limitação de escopos do Google é uma daquelas partes em que o OIDC complica tudo. Idealmente, o Google deveria retornar um token de ID em vez de enfiar isso no token de acesso, mas isso é um problema da configuração OAuth do Google, não uma característica da 8628 em si
      É daí que vem a complexidade sem fim do OAuth. O padrão define bem a estrutura de como montar e transportar um esquema de autorização, mas deliberadamente não diz nada sobre o que ele deve ser. Foram necessários a invenção do OIDC e vários anos até obter um conjunto comum de endpoints HTTP com o qual “a maioria” dos provedores concorda
  • Outro hack é encaminhar a chamada xdg-open do servidor para o notebook. Fiz uma ferramenta pequena para minha infraestrutura pessoal que faz isso: https://github.com/zimbatm/subportal/

  • Não daria para combinar as duas abordagens? Redirecionar para uma URL em localhost e fazer ela devolver hello; se o cliente não receber o hello, a CLI imprime a URL
    Ao mesmo tempo, se o servidor não receber a resposta ao hello que enviou, ele pode mostrar um código no navegador com uma mensagem como “confirme se você está tentando fazer login”. Também daria para facilitar mais, como o Google faz ao mostrar números para escolher no celular

    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  
    

    A vantagem é que, mesmo no caso 2, as pessoas clicam em links com facilidade, mas compartilhar OTP/código tende a ser menos comum, e o atacante teria de continuar intervindo com engenharia social durante o ataque

  • Quando funciona bem na máquina local, não precisa de interação, então seria bom que o padrão fosse o fluxo baseado no navegador

    • Esse fluxo também funciona no navegador quando dá certo. A diferença é que, quando falha, existe um caminho alternativo melhor