CLI 인증, 올바른 방식

13 hours ago 6
  • 많은 CLI는 노트북의 로컬 브라우저에서는 빠르게 끝나는 localhost OAuth 리다이렉트를 기본값으로 쓰지만, SSH·컨테이너·WSL 같은 개발 환경에서는 같은 가정이 깨져 로그인 흐름이 멈춤
  • 현재 방식은 CLI가 127.0.0.1에 임시 HTTP 서버를 열고 브라우저를 인증 URL로 보낸 뒤, 인증 제공자가 authorization code를 로컬 callback으로 돌려주는 구조임
  • 2019년 표준화된 RFC 8628 Device Authorization Grant는 토큰을 요청하는 CLI와 사용자가 인증하는 브라우저 장치를 분리해, 포트 바인딩이나 로컬 브라우저 의존을 없앰
  • Device flow는 device_code, user_code, verification_uri, interval을 받아 /token을 주기적으로 폴링하고, authorization_pending, slow_down, access_denied, expired_token 같은 표준 상태를 처리함
  • 새 CLI라면 device flow를 기본값으로 두고 .well-known/openid-configuration으로 엔드포인트를 발견하며, refresh token은 ~/.config의 JSON 파일이 아니라 OS keychain에 저장해야 함

localhost 리다이렉트가 전제하는 것

  • 흔한 CLI 로그인은 로컬 HTTP 서버와 시스템 브라우저가 같은 머신에 있다는 가정 위에서 동작함
    • CLI가 127.0.0.1의 특정 포트에 HTTP 서버를 바인딩함
    • 시스템 브라우저를 OAuth authorization endpoint로 열고 redirect_uri=/callback">http://127.0.0.1:<port>/callback을 포함함
    • 사용자가 로그인하면 인증 제공자가 authorization code를 loopback URL로 302 리다이렉트함
    • CLI의 작은 HTTP 서버가 code를 읽고 token endpoint에서 토큰으로 교환함
    • 대부분 PKCE가 붙고, 이후 “이 탭을 닫아도 됩니다” 페이지가 표시됨
  • gcloud auth login, wrangler login, 이전 vercel login과 여러 vendor CLI가 이 방식을 사용함
    • Wrangler는 8976 포트를 사용함
    • gcloud는 8085를 사용함
    • Claude Code는 실행할 때마다 임시 포트를 잡음
  • RFC 8252는 native app에서 브라우저가 있는 경우 이 패턴을 권장하지만, 호스트에 브라우저가 없을 때의 처리는 다루지 않음

사용자가 localhost 단계를 잘 못 보는 이유

  • localhost callback은 매우 짧게 지나가므로 대부분 사용자가 보지 못함
  • CLI가 출력하는 URL은 길고, 그 안에 redirect URI가 query string으로 들어 있음
  • 사용자는 인증 제공자의 실제 도메인에서 로그인하고 승인함
  • 인증 제공자는 브라우저를 localhost callback으로 보낸 뒤 CLI가 code를 읽게 하고, 다시 polished “signed in” 페이지로 이동시킴
  • 겉으로는 “웹사이트에서 로그인했더니 CLI가 인증됐다”처럼 보이지만, 실제로는 로컬 HTTP 서버와 브라우저의 공존이 흐름을 지탱함

SSH·컨테이너·WSL에서 깨지는 지점

  • 전체 흐름은 CLI가 실행되는 머신과 브라우저가 실행되는 머신이 같다는 가정에 의존함
  • SSH 세션에서는 원격 호스트에 브라우저가 없고, xdg-open이 실패하거나 X forwarding 환경에서 보이지 않는 원격 브라우저를 열 수 있음
    • callback 포트를 노트북으로 터널링할 수는 있지만, 인증 제공자에 등록된 redirect URI가 터널을 통과한 포트를 허용해야 함
  • 컨테이너에는 브라우저가 없고, 많은 이미지에는 xdg-open이나 open도 없음
    • -p로 callback 포트를 노출할 수 있지만 CLI가 어떤 포트를 잡을지 알아야 함
    • Cloudflare CLI에는 이 문제로 막힌 사용자의 이슈가 이어짐
  • WSL에서는 브라우저가 Windows에서 열리고 loopback server는 Linux에서 실행됨
    • WSL2의 포트 포워딩은 대부분 동작하지만 항상 그렇지는 않음
  • shared box에서는 같은 머신의 다른 프로세스가 /proc/net/tcp로 listening port를 찾거나, 알려진 포트를 먼저 바인딩하려고 경쟁할 수 있음
    • PKCE는 code exchange를 보호하지만 redirect 자체의 authenticated session을 보호하지 않음

fallback이 이미 드러내는 설계 문제

  • loopback 흐름을 기본으로 제공하는 CLI들은 깨질 때를 위한 fallback도 함께 제공함
  • gcloud에는 --no-launch-browser가 있음
  • Wrangler는 멈추며, 수용된 workaround는 두 번째 터미널에서 localhost URL을 직접 curl하는 방식임
  • Anthropic의 claude는 “Paste code here if prompted”를 출력하고 기다림
  • 이런 fallback은 사실상 수동 device flow이며, CLI가 실제로 쓰이는 환경에서 기본 흐름이 동작하지 않기 때문에 존재함

RFC 8628 Device Authorization Grant

  • RFC 8628은 2019년에 “input-constrained devices”를 위해 나온 OAuth 2.0 Device Authorization Grant임
    • TV, 콘솔, CLI가 대상에 포함됨
    • 토큰을 요청하는 장치와 사용자가 인증하는 장치를 분리하는 것이 핵심임
  • CLI는 인증 제공자의 device_authorization_endpoint에 POST함
    • 예시 요청은 client_id=my-cli&scope=openid+offline_access를 전송함
  • 인증 제공자는 다음 값을 포함한 JSON을 반환함
    • device_code
    • user_code
    • verification_uri
    • verification_uri_complete
    • expires_in
    • interval
  • CLI는 URL과 짧은 코드를 출력하고, 가능하면 verification_uri_complete에 대한 QR도 보여줌
  • 사용자는 원하는 장치에서 URL을 열고 로그인한 뒤, 요청 scope와 client name을 보고 CLI에 표시된 짧은 코드와 일치하는지 확인한 후 승인함

폴링과 표준 상태 처리

  • CLI는 token endpoint를 interval초마다 폴링함
  • grant type은 urn:ietf:params:oauth:grant-type:device_code를 사용함
  • RFC 8628 section 3.5는 다음 상태를 정의함
    • authorization_pending: 사용자의 승인을 기다리는 상태
    • slow_down: 인증 제공자가 폴링 간격을 늦추라고 요청한 상태이며, 명세는 interval을 최소 5초 늘리라고 명시함
    • access_denied: 사용자가 거부한 상태
    • expired_token: 너무 오래 기다려 토큰이 만료된 상태
  • device flow에서는 CLI가 포트를 바인딩하지 않고, 실행 호스트에 브라우저가 있다고 가정하지 않음
  • 같은 로그인 방식이 노트북, 컨테이너, 사람의 승인을 기다리는 CI job에서 동작함

폴링 비용과 엔드포인트 발견

  • 기본 폴링 interval은 5초임
  • 대부분 인증은 1분 이내에 끝나므로 일반적인 로그인은 /token에 약 10번 정도 폴링하고 멈춤
  • 서버는 slow_down으로 interval을 늘릴 수 있고, 잘 작성된 client는 이를 따라야 함
  • pending login마다 stateful endpoint에 WebSocket이나 SSE 연결을 유지하는 방식과 비교하면, /token에 대한 stateless polling이 더 단순하고 저렴함
  • 인증 제공자가 OpenID Connect Discovery를 지원하면 CLI는 .well-known/openid-configuration에서 device_authorization_endpoint와 token_endpoint를 가져와 URL 하드코딩을 없앨 수 있음

device flow의 피싱 위험

  • device flow에는 공격자가 실제 인증 제공자의 device_authorization_endpoint를 호출해 user_code와 device_code를 받은 뒤 피해자에게 입력을 유도하는 공격이 있음
  • 피해자는 실제 URL에서 실제 코드로 로그인하고 실제 consent screen을 승인할 수 있음
  • 공격자는 자신이 생성한 device_code로 /token을 폴링하다가 access token을 받음
  • 러시아 threat actor는 2024년 8월 이후 M365 tenant를 상대로 이 캠페인을 수행함
    • Microsoft Threat Intelligence는 이를 Storm-2372로 추적함
    • Volexity는 APT29/Midnight Blizzard로 attribution함
    • 정부, 방위, NGO tenant가 여러 대륙에서 영향을 받음

피싱 방어는 인증 제공자 책임

  • 피싱 방어는 CLI가 아니라 인증 제공자 쪽에서 이뤄져야 함
  • 필요한 완화책은 다음과 같음
    • 짧은 user_code 만료 시간
    • verification page에서 client name과 요청 위치를 눈에 띄게 표시
    • code 입력 시도에 대한 rate limiting
    • verification_uri_complete를 노출하지 않아 피해자가 링크를 클릭하는 대신 코드를 직접 입력하게 함
    • 고가치 tenant에서는 known network나 device가 아니면 device code flow를 막는 conditional access policy
  • CLI의 역할은 명세를 따르고 shortcut을 만들지 않는 것임
  • device flow는 local attack surface를 social attack surface로 바꾸지만, 더 많은 환경에서 동작하는 흐름을 제공하고 인증 제공자의 mitigation을 활용하는 쪽이 더 적절함

Go 구현 예시의 핵심 흐름

  • 전체 구현은 Go에서 net/http만으로 약 30줄에 들어감
  • 구현 흐름은 다음과 같음
    • client_id와 scope를 담아 DeviceAuthorizationEndpoint에 http.PostForm 호출
    • 응답 JSON에서 DeviceCode, UserCode, VerificationURIComplete, Interval을 디코딩
    • 사용자에게 VerificationURIComplete와 UserCode를 출력
    • TokenEndpoint에 device_code, client_id, device grant type을 넣어 반복 POST
    • authorization_pending이면 계속 대기
    • slow_down이면 interval을 5초 늘림
    • error가 없으면 access_token과 refresh_token을 반환
    • 다른 error는 실패로 처리
  • Keycloak realm에서 “OAuth 2.0 Device Authorization Grant” capability를 켜거나, grant를 지원하는 OpenID-certified provider를 쓰면 device-flow login이 동작함

새 CLI의 기본값으로 삼을 방식

  • 기본값은 device flow로 설정해야 함
  • .well-known/openid-configuration에서 엔드포인트를 발견해 URL을 하드코딩하지 않아야 함
  • interval과 slow_down을 반드시 지켜야 함
  • refresh token은 ~/.config 아래 JSON 파일이 아니라 OS keychain에 저장해야 함
  • 빠른 노트북 로그인을 위해 loopback path를 제공하고 싶다면 --web flag 뒤에 두고 기본값으로 만들지 않아야 함

이미 옮겨간 CLI와 남은 도구들

  • device flow를 기본값으로 쓰는 CLI들이 있음
    • gh auth login은 처음부터 device flow를 사용했으며, open source에서 가장 깔끔한 reference implementation으로 평가됨
    • aws sso login은 IAM Identity Center를 상대로 device flow를 end-to-end로 실행함
    • vercel login은 2025년 9월 RFC 8628로 이동하며 email-based login과 이전 --oob flag를 대체함
    • Stripe CLI는 RFC 8628 자체는 아니지만 UX를 잘 구현한 pairing-code flow를 사용함
  • 여전히 loopback flow를 기본으로 두고 paste-the-code fallback을 붙인 도구들도 있음
    • Google gcloud
    • Cloudflare wrangler
    • Anthropic claude
  • CLI가 노트북을 벗어날 때마다 수동 paste-the-code fallback이 필요하다면, 그 fallback을 기본 흐름으로 제공하는 편이 맞음
Read Entire Article