Add service account authentication - #23
Conversation
Authenticate with an APImetrics service account instead of the browser login, which is what makes the CLI usable from CI and other headless environments. Point `--service-account` (or the environment variable `<APP_NAME>_SERVICE_ACCOUNT`) at the JSON file the web app issues and every request uses the OAuth 2.0 client credentials grant. The client credentials grant issues no refresh token (RFC 6749 4.4.3) and Auth0 rejects `offline_access` for it, so the hour-long access token is cached and simply re-requested on expiry. Tokens are cached under a key derived from the credentials rather than the profile, so a service account never picks up the interactive login's token or another account's, and rotating a secret invalidates the cache instead of silently serving the old token. Also add `--project-id` / `<APP_NAME>_PROJECT_ID`. Service accounts are granted access to specific projects and headless runs cannot answer the interactive project picker, so without this the first call in CI hangs on a prompt. The override applies to a single run and never overwrites the project saved by `project select`; a non-TTY run with no project now fails with an actionable message rather than a dead prompt. Global flags are parsed exactly once, in Run. Parsing them a second time would append to the repeatable flags, doubling every `-H` and `-q` value, so the service account and project overrides are applied there rather than in Init. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds headless authentication to the APImetrics CLI via a downloaded Service Account JSON file, enabling CI/cron usage without browser-based login, and introduces a non-interactive-friendly project selection override.
Changes:
- Add an OAuth2 client-credentials based “service account” auth handler with token caching keyed by credentials.
- Add
--service-account/APIMETRICS_*_SERVICE_ACCOUNTand--project-id/APIMETRICS_*_PROJECT_IDto support headless runs and avoid interactive prompts. - Update documentation and gitignore to reflect new workflows and per-environment build outputs.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents service account usage, project override, env vars, and logout semantics. |
| oauth/serviceaccount.go | Implements token source + request handler for service-account auth with cache-key override support. |
| oauth/serviceaccount_test.go | Adds unit tests validating token request shape and handler caching behavior. |
| main.go | Registers the new oauth-service-account auth scheme. |
| cli/state.go | Adds per-run project override resolution and active project selection logic. |
| cli/serviceaccount.go | Adds service account file loading/validation and credential-derived cache key generation. |
| cli/serviceaccount_test.go | Adds tests for file loading/defaulting, cache key derivation, and profile override behavior. |
| cli/config.go | Updates project selection behavior for non-TTY runs; adjusts login/logout messaging and service-account logout behavior. |
| cli/cli.go | Ensures eager flag parsing happens once and applies service account + project overrides before first API call. |
| cli/apiconfig.go | Refactors default profile init and adds applyCredentialOverrides() for service account + project header overrides. |
| .gitignore | Anchors ignore rules for root-level environment-specific binaries. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Include the audience in the service account's token cache key. The file format lets a service account name its own audience, so two files sharing a client id, secret and token URL but naming different audiences would otherwise share a cache entry and be handed a token minted for the wrong audience. Clear the `.refresh` entry in `logout --service-account` alongside the rest. The client credentials grant issues no refresh token, but TokenHandler writes one whenever the endpoint returns it and reads it back when rebuilding a cached token, so leaving it behind would let a stale entry outlive a logout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
cli/serviceaccount_test.go:26
- This test mutates the global buildCfg (TokenURL/AuthAudience) but never restores it, which can make other tests order-dependent if they rely on the default build config. Capture the previous values and restore them with t.Cleanup (or a defer) to keep the test isolated.
This issue also appears in the following locations of the same file:
- line 47
- line 60
- line 117
buildCfg.TokenURL = "https://auth.example.com/oauth/token"
buildCfg.AuthAudience = "https://api.example.com"
cli/serviceaccount_test.go:49
- This test mutates the global buildCfg (TokenURL/AuthAudience) but does not restore it, which can leak state into other tests in the package. Use t.Cleanup to restore prior values after the test completes.
buildCfg.TokenURL = "https://auth.example.com/oauth/token"
buildCfg.AuthAudience = "https://api.example.com"
cli/serviceaccount_test.go:61
- This test changes the global buildCfg.TokenURL but never restores it, which can make other tests depend on execution order. Save the old value and restore it via t.Cleanup.
buildCfg.TokenURL = "https://auth.example.com/oauth/token"
cli/serviceaccount_test.go:119
- This test mutates the global buildCfg (TokenURL/AuthAudience) without restoring it, which risks order-dependent behavior in other tests that call Init/initAPIConfig. Restore the prior values with t.Cleanup.
buildCfg.TokenURL = "https://auth.example.com/oauth/token"
buildCfg.AuthAudience = "https://api.example.com"
Summary
Adds support for the new Service Account functionality: point the CLI at the JSON file the APImetrics web app issues and it authenticates every request with the OAuth 2.0 client credentials grant instead of the browser-based sign-in. This is what makes the CLI usable from CI, cron jobs, and other headless environments.
The
client_id,client_secretandaudiencecome from the file; the token URL comes from the build config, so a QC service account works with the QC build. Atoken_urlin the file overrides it if one ever appears there.No refresh tokens
Verified against
qc-auth.apimetrics.io: the client credentials grant issues no refresh token, and asking foroffline_accessis rejected outright.This is per RFC 6749 4.4.3 — the credentials are themselves long-lived, so there is nothing for a refresh token to buy. The hour-long access token is cached and simply re-requested on expiry.
Tokens cache under
service-account:<hash>, keyed on the credentials rather than the profile, so a service account never picks up the interactive login's token or another account's. The secret is part of the hash, so rotating it invalidates the cache instead of silently serving the old token.--project-idOne addition beyond the auth change, flagged for review. Service accounts are granted access to specific projects, and a headless run can't answer
ensureProject's interactive project picker — without this the first call in CI hangs on a prompt, which defeats the point.--project-id/<APP_NAME>_PROJECT_IDapplies to a single run and never overwrites the project saved byproject select. A non-TTY run with no project now fails with an actionable message rather than a dead prompt.Bug found along the way
The first version resolved the service account in
Initso auth was settled before Cobra ran, which meantGlobalFlagswas parsed there and inRun. pflag appends on a repeat parse, so every-Hand-qvalue was silently doubled (?foo=bar&foo=bar). The parse now stays exactly where it was, inRun, with the overrides applied immediately after it viaapplyCredentialOverrides().Testing
go test ./...passes. New unit tests cover file loading and validation, cache-key derivation, the profile override, the token request shape, and the handler's caching and validation branches.Manually verified end-to-end against QC with a real service account:
login,list-calls,list-schedules,list-account-projects,project show-H/-qno longer duplicatedAlso
.gitignorenow covers the per-environment build outputs (apimetrics-qc,apimetrics-beta, ...). Both binary rules are anchored to the repo root so they can't swallow a same-named file deeper in the tree.🤖 Generated with Claude Code