CLI 인증, 올바른 방식
12 hours ago
5
많은 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를 상대로 이 캠페인을 수행함
피싱 방어는 인증 제공자 책임
피싱 방어는 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을 기본 흐름으로 제공하는 편이 맞음
Homepage
Tech blog
CLI 인증, 올바른 방식
🔉 볼륨 줄이기
🔊 볼륨 키우기
🔇 음소거
⏭️ 다음 곡