Autenticação de CLI, do jeito certo
(abgeo.dev)- 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_urieinterval, faz polling periódico em/tokene trata estados padrão comoauthorization_pending,slow_down,access_deniedeexpired_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
302o 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”
- A CLI faz bind de um servidor HTTP em uma porta específica de
gcloud auth login,wrangler login, versões antigas devercel logine 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 Wrangler usa a porta
- 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-openpode 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-openouopen- É 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
- É possível expor a porta de callback com
- 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/tcpou 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
curlmanualmente na URL de localhost a partir de um segundo terminal - O
claudeda 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_endpointdo provedor de autenticação- Um exemplo de requisição envia
client_id=my-cli&scope=openid+offline_access
- Um exemplo de requisição envia
- O provedor responde com um JSON contendo
device_codeuser_codeverification_uriverification_uri_completeexpires_ininterval
- 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
intervalsegundos - 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árioslow_down: o provedor pede para reduzir a frequência do polling, e a especificação exige aumentar o interval em pelo menos 5 segundosaccess_denied: o usuário recusouexpired_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
/tokene 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_endpointetoken_endpointde.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_endpointdo provedor real, recebeuser_codeedevice_codee 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
/tokencom odevice_codeque 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
- tempo curto de expiração do
- 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.PostFormnoDeviceAuthorizationEndpointcomclient_idescope - decodificar do JSON de resposta
DeviceCode,UserCode,VerificationURICompleteeInterval - exibir
VerificationURICompleteeUserCodeao usuário - fazer POST repetidamente no
TokenEndpointcomdevice_code,client_ide 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_tokenerefresh_token - qualquer outro erro deve ser tratado como falha
- chamar
- 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 intervaleslow_downprecisam 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 loginusa device flow desde o começo e é considerado uma das implementações de referência mais limpas em open sourceaws sso loginexecuta o device flow de ponta a ponta com o IAM Identity Centervercel loginmigrou 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
- Google
- 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
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
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-opene faz encaminhamento automático de portas para mascarar a experiência ruim: https://github.com/phinze/bankshotInteressante. 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
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-opendo 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
localhoste fazer ela devolverhello; se o cliente não receber ohello, a CLI imprime a URLAo mesmo tempo, se o servidor não receber a resposta ao
helloque 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 celularA 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