Skip to content

Add device code login (apimetrics login --device) - #30

Merged
ndenny merged 2 commits into
developfrom
feat/device-code-login
Oct 19, 2026
Merged

ndenny merged 2 commits into
developfrom
feat/device-code-login

Conversation

@ndenny

@ndenny ndenny commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

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 the code= 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.go adds DeviceCodeTokenSource and a DeviceCodeHandler, registered as oauth-device-code in main.go.
    • It requests a device code from /oauth/device/code on the token URL's host (Auth0's layout); a device_url parameter overrides that.
    • The audience is read from the audience query parameter on authorize_url, which is where the browser login carries it.
    • Polling follows RFC 8628 §3.5: it waits the server's interval (5 seconds if none is given), keeps waiting on authorization_pending, adds 5 seconds on slow_down, and stops on access_denied, expired_token, any other error, or once the code's expires_in has passed.
    • Like the browser login, it is wrapped in 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 of requestToken into postForm and parseToken, which the device flow shares. requestToken behaves as before, except that it now returns an error if reading the response body fails instead of ignoring it.
  • cli/config.go: login gets a --device flag. It switches that login to oauth-device-code with the same parameters and the same cache key (apimetrics:default) as the browser login, so every later command uses the token it gets. Combining --device with --service-account is an error.
  • README.md: documents login --device under Getting Started.

Limits

  • Only login --device uses 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.
  • Like plain login, login --device returns straight away without prompting if a valid token is already cached. Run apimetrics logout first to force a fresh login.

Auth0 configuration required

On each tenant (prod auth.apimetrics.io, which beta shares, QC qc-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 --device fails with Auth0's error from the device authorization endpoint, and the browser login is unaffected.

Testing

  • oauth/devicecode_test.go runs against a fake Auth0 server, with the sleep between polls replaced so the tests don't wait:
    • polling through authorization_pending and slow_down to a token, checking the forms sent and the intervals waited (5s, 5s, 10s);
    • stopping after one poll on access_denied, expired_token, another OAuth error, and a non-JSON reply;
    • giving up once the code's lifetime has passed (900s at 5s means 180 polls);
    • deriving the device URL from the token URL, and the audience from the authorize URL.
  • cli/serviceaccount_test.go: login --device calls the device handler with the browser login's cache key and parameters, and --device together with a service account is rejected.
  • go test ./... passes.
  • Not tested against a live Auth0 tenant, because the Device Code grant isn't enabled on any of them yet.

🤖 Generated with Claude Code

`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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 from authorize_url and device URL derived from token_url, wrapped in RefreshTokenSource.
  • login gains a --device flag (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 in main.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

Comment thread oauth/devicecode.go Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@ndenny
ndenny merged commit 674aa98 into develop Oct 19, 2026
2 of 3 checks passed
@ndenny
ndenny deleted the feat/device-code-login branch October 19, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants