Repository navigation
Add device code login (apimetrics login --device) - #30
Conversation
`apimetrics login --device` uses the OAuth 2.0 device authorization grant (RFC 8628): the CLI prints a URL and a short code, the user approves it in a browser on any device, and the CLI polls the token endpoint until they do. Unlike the browser login it needs no redirect to localhost:8484, so it works over SSH and in containers. The new oauth-device-code handler takes the browser login's parameters and cache key, so the token it gets is used by every later command, and it tries a cached refresh token first like the browser login does. The device endpoint is derived from the token URL (Auth0's /oauth/device/code) and the audience from the authorize URL's query string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new authentication grant flow that is security-sensitive and, per the description, untested against any live Auth0 tenant, so it warrants human review.
1 open issue
What changed in this PR
This PR adds apimetrics login --device, implementing the OAuth 2.0 device authorization grant (RFC 8628). It gives users a way to authenticate in environments where the existing browser login's http://localhost:8484 redirect can't be reached (SSH sessions, containers, remote desktops): the CLI prints a URL and short code, the user approves in a browser on any device, and the CLI polls the token endpoint until approval. It fits alongside the existing oauth-authorization-code and oauth-service-account handlers, reusing the browser login's config parameters and cache key so subsequent commands transparently pick up the token.
Changes:
- New
DeviceCodeTokenSource/DeviceCodeHandler(oauth/devicecode.go) with device-code request, RFC 8628 polling (authorization_pending/slow_down/terminal errors/expiry), audience derived fromauthorize_urland device URL derived fromtoken_url, wrapped inRefreshTokenSource. logingains a--deviceflag (cli/config.go) that switches to the device handler with the browser login's params/cache key and rejects combination with--service-account; handler registered inmain.go.- Tests (
oauth/devicecode_test.go,cli/serviceaccount_test.go) plus README documentation.
| File | Description |
|---|---|
| oauth/devicecode.go | New device-code token source and handler implementing RFC 8628 polling, audience/device-URL derivation, and refresh-token wrapping. |
| oauth/devicecode_test.go | Tests polling-to-approval, terminal errors, expiry give-up, and URL/audience derivation against a fake Auth0 server. |
| cli/config.go | Adds --device flag to login, routing to oauth-device-code and rejecting --service-account combination. |
| cli/serviceaccount_test.go | Tests that --device dispatches to the device handler with the browser login's key/params and is rejected with a service account. |
| main.go | Registers the oauth-device-code auth handler. |
| README.md | Documents login --device under Getting Started. |
Review effort: Balanced
Address Copilot review: requestDeviceToken duplicated requestToken's HTTP call and its response parsing. Both now use postForm and a new parseToken helper in request.go. requestToken now also returns a body read error rather than ignoring it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces a new, security-sensitive OAuth device authentication flow that cannot be validated against a live Auth0 tenant and warrants final human review despite being well-structured and tested.
No open issues 🎉
1 resolved since last review
Review effort: Balanced

Summary
Adds
apimetrics login --device, which logs in with the OAuth 2.0 device authorization grant (RFC 8628). The CLI prints a URL and a short code; the user opens the URL in a browser on any device, checks the code and approves; the CLI polls the token endpoint until they do.The existing browser login redirects back to
http://localhost:8484. That fails over SSH, in containers and on remote desktops, where the browser runs on a different machine. Today the only workaround is copying thecode=value out of the failed page's address bar. The device flow needs no redirect, so it works in all of those places.How it works
oauth/devicecode.goaddsDeviceCodeTokenSourceand aDeviceCodeHandler, registered asoauth-device-codeinmain.go./oauth/device/codeon the token URL's host (Auth0's layout); adevice_urlparameter overrides that.audiencequery parameter onauthorize_url, which is where the browser login carries it.interval(5 seconds if none is given), keeps waiting onauthorization_pending, adds 5 seconds onslow_down, and stops onaccess_denied,expired_token, any other error, or once the code'sexpires_inhas passed.RefreshTokenSource, so a cached refresh token is tried before asking for a new code.oauth/request.go: the form POST and the token-response parsing are split out ofrequestTokenintopostFormandparseToken, which the device flow shares.requestTokenbehaves as before, except that it now returns an error if reading the response body fails instead of ignoring it.cli/config.go:logingets a--deviceflag. It switches that login tooauth-device-codewith the same parameters and the same cache key (apimetrics:default) as the browser login, so every later command uses the token it gets. Combining--devicewith--service-accountis an error.README.md: documentslogin --deviceunder Getting Started.Limits
login --deviceuses the device flow. A login that a command triggers on its own, when there is no valid cached token, still uses the browser flow. Once refresh tokens work (Request offline_access so browser logins get a refresh token #29 plus the Auth0 settings there), that only happens when the refresh token itself has expired.login,login --devicereturns straight away without prompting if a valid token is already cached. Runapimetrics logoutfirst to force a fresh login.Auth0 configuration required
On each tenant (prod
auth.apimetrics.io, which beta shares, QCqc-auth.apimetrics.io, and dev), enable Applications → the CLI application → Advanced Settings → Grant Types → Device Code. Auth0 only offers this grant to Native applications. Until it is enabled,login --devicefails with Auth0's error from the device authorization endpoint, and the browser login is unaffected.Testing
oauth/devicecode_test.goruns against a fake Auth0 server, with the sleep between polls replaced so the tests don't wait:authorization_pendingandslow_downto a token, checking the forms sent and the intervals waited (5s, 5s, 10s);access_denied,expired_token, another OAuth error, and a non-JSON reply;cli/serviceaccount_test.go:login --devicecalls the device handler with the browser login's cache key and parameters, and--devicetogether with a service account is rejected.go test ./...passes.🤖 Generated with Claude Code