feat(auth): implement Device Code login flow and token refresh - #4
feat(auth): implement Device Code login flow and token refresh#4emsearcy wants to merge 17 commits into
Conversation
Implements `lfx auth login`, `lfx auth token`, `lfx auth status`, and `lfx auth logout` on top of golang.org/x/oauth2's device authorization grant support (Config.DeviceAuth/DeviceAccessToken) and refresh-token TokenSource, targeting the static per-environment Auth0 client IDs provisioned in LFXV2-2513. - internal/auth0: new package resolving --env to its IdP domain and compiled-in client ID, and driving the device code request, poll, and refresh-token exchange. - internal/commands/auth.go: real login (interactive device code flow or --with-token for headless use), token (cached-token fast path, refresh-and-recache on expiry, clear error on invalid/expired refresh token), status, and logout implementations. - internal/credstore: extends DeviceState with environment/audience (needed to replay the same IdP/client on refresh) and adds DeleteDeviceState for logout. Deliberately does not persist a device ID: Auth0's device flow has no such concept, and the `gh` CLI precedent cited when this story was written turned out to be an unrelated telemetry identifier, not part of its OAuth flow. - Includes outstanding .cspell.json wordlist additions. LFXV2-2515, LFXV2-2516 Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Implements Auth0 device-code authentication and token refresh for the LFX CLI.
Changes:
- Adds environment-aware Auth0 device authorization and refresh support.
- Implements login, token, status, logout, and credential-state cleanup.
- Adds OAuth dependency and spellcheck terms.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
internal/auth0/device.go |
Adds Auth0 device and refresh flows. |
internal/commands/auth.go |
Implements authentication commands. |
internal/credstore/credstore.go |
Extends and deletes persisted device state. |
go.mod |
Adds OAuth2 dependency. |
go.sum |
Records OAuth2 checksums. |
.cspell.json |
Adds required technical terms. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- auth token: use auth0.Resolve's domain (trusted) for the refresh client instead of the persisted state.json value, while still sanity-checking it against the stored domain to catch a tampered or corrupted state file before it can redirect a refresh request. - credstore: add DeviceState.Insecure, recording which credential backend (keychain vs. --insecure-storage) wrote state.json. auth token/status/logout now validate this against the invocation's own --insecure-storage flag before trusting or deleting state.json, so switching backends no longer silently corrupts or destroys the other backend's metadata. - README.md/AGENTS.md: update the stale "auth commands are stubs" note now that lfx auth is fully implemented; only lfx api remains a stub. - auth0: map DeviceAccessToken's context.DeadlineExceeded (returned once the device code's own expiry passes, ahead of the token endpoint ever reporting expired_token) to ErrExpiredToken, so the documented error and "device code expired" message are reliably produced. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/auth0/device.go:184
- The caller can pass a context with its own deadline to
RequestDeviceCode;DeviceAccessTokenderives its polling context from that parent, soDeadlineExceededis not necessarily caused by the device-code expiry. This branch misreports an earlier caller timeout as an expired device code. Checkdc.ctx.Err()(or otherwise distinguish the parent deadline) before mapping the error toErrExpiredToken.
// passes in (see RequestDeviceCode), any DeadlineExceeded seen here
// can only come from that internal one, so it's safe to always treat
// it as ErrExpiredToken.
if errors.Is(err, context.DeadlineExceeded) {
return nil, ErrExpiredToken
internal/commands/auth.go:186
verification_uri_completeis optional, but both the browser and manual paths use it unconditionally. A valid device response that only suppliesverification_uritherefore opens/prints an empty URL and leaves the user unable to complete login. Fall back todc.VerificationURIwhen the complete URI is empty.
fmt.Printf("Opening %s in your browser...\n", dc.VerificationURIComplete)
if err := openBrowser(dc.VerificationURIComplete); err != nil {
lfx auth login --env prod was sending device code and token requests to linuxfoundation.auth0.com, but end users only ever authenticate at the prod tenant's custom domain, sso.linuxfoundation.org (see auth0-terraform's auth0_domain variable). The mismatch surfaced as an Auth0 invalid_request: Missing required parameter: response_type error completing login, and a login that never resolved when polling. Since this Client only talks to the device code and token endpoints (never the Auth0 Management API), there's no need to separately track each environment's underlying tenant name -- only the IdP domain end users authenticate against, which for prod is the custom domain rather than the *.auth0.com domain. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
- Poll(): distinguish a caller-supplied context deadline from DeviceAccessToken's own internally-derived one before mapping DeadlineExceeded to ErrExpiredToken, so an unrelated caller timeout is no longer misreported as an expired device code. - loginWithDeviceCode: fall back to VerificationURI when VerificationURIComplete is empty (it's optional per RFC 8628 section 3.2), so a login doesn't try to open/print an empty URL. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
|
AI-assisted: Fixed in d94eda8 — addressed the 2 suppressed Copilot comments from the follow-up review:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (5)
internal/commands/auth.go:216
- As in the supplied-token path, credentials are saved before the required device state. A state-file write failure therefore reports login failure while leaving newly issued credentials paired with missing or previous environment metadata. Make the two persistence updates atomic from the caller’s perspective or restore/remove the credential update on failure.
if err := store.SaveCredentials(credstore.Credentials{
RefreshToken: token.RefreshToken,
AccessToken: token.AccessToken,
AccessTokenExpiry: token.Expiry,
}); err != nil {
return fmt.Errorf("save credentials: %w", err)
}
internal/commands/auth.go:209
- A successful Auth0 device exchange can omit
refresh_tokenwhen the selected custom API does not allow offline access, even thoughoffline_accesswas requested. This currently persists an empty refresh token and reports “Login successful,” so the session becomes unusable as soon as the cached access token expires. Reject the login with a clear configuration error whentoken.RefreshTokenis empty before saving credentials.
return errors.New("login was denied")
case errors.Is(err, auth0.ErrExpiredToken):
return errors.New("device code expired before login completed")
default:
return err
internal/commands/auth.go:152
- Credentials have already been overwritten when
SaveDeviceStateruns. If writingstate.jsonfails, the command returns an error but leaves the new refresh token paired with missing or stale environment metadata, so a later refresh cannot safely reconstruct its Auth0 client. Persist these as one consistent operation or roll back the credential update when state persistence fails.
This issue also appears on line 210 of the same file.
if err := store.SaveDeviceState(credstore.DeviceState{
IDPDomain: domain,
Environment: string(env),
Audience: audience,
Insecure: insecure,
}); err != nil {
return fmt.Errorf("save device state: %w", err)
AGENTS.md:50
- This implementation now uses compiled-in static client IDs, but AGENTS.md:177-180 still says the gh-pages CIMD URL is the Device Code client ID. That guidance directly contradicts
internal/auth0/device.go:22-26and can send future maintainers to update an unused client definition. Update the gh-pages section to explain that the CIMD asset is no longer used by this flow.
`lfx auth login` / `status` / `token` / `logout` are fully implemented,
including the Auth0 Device Code flow, refresh-token exchange, and
internal/auth0/device.go:91
- All production callers leave
HTTPClientnil, andmainsuppliescontext.Background(), so device-code and refresh requests usehttp.DefaultClientwithout an overall response timeout. An Auth0 endpoint that accepts a connection but stalls can therefore hanglfx auth loginorlfx auth tokenindefinitely. Provide a bounded default client or add per-request deadlines while retaining the device-flow polling deadline.
// HTTPClient is used for all requests. Defaults to
// http.DefaultClient if nil.
HTTPClient *http.Client
lfx auth login's "Logged in as ..." message now prefers the LFID username (the https://sso.linuxfoundation.org/claims/ custom ID token claim) over email, since username is the conventional LFX identifier. Shows "username (email)" when both are present, falling back to "email (no username)" in the unexpected case the custom claim is missing, and finally the subject claim if neither is present. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
internal/commands/auth.go:223
- This has the same partial-login failure mode as
--with-token: credentials are replaced before device state is saved. If the state write fails, an older same-backend state remains usable and can direct this new refresh token to the previous environment's token endpoint. Roll back/delete the new credentials on failure, or make persistence of credentials and their routing metadata atomic.
if err := store.SaveDeviceState(credstore.DeviceState{
IDPDomain: domain,
Environment: string(env),
Audience: audience,
Insecure: insecure,
}); err != nil {
return fmt.Errorf("save device state: %w", err)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (5)
internal/commands/auth.go:146
SaveCredentialshas already replaced the selected backend's credentials whenSaveDeviceStatefails. The command then reports a failed login while leaving the new refresh token paired with stale or missing environment metadata, so a later refresh can target the wrong configured tenant and fail. Persist the credential/state pair transactionally, or restore/delete the newly written credentials when saving the state fails.
if err := store.SaveCredentials(credstore.Credentials{RefreshToken: refreshToken}); err != nil {
return fmt.Errorf("save credentials: %w", err)
}
if err := store.SaveDeviceState(credstore.DeviceState{
internal/commands/auth.go:217
- This second write can fail after the newly issued credentials have already been saved, leaving credentials and device metadata from different login attempts. A later refresh then uses stale metadata with the new refresh token. Make these writes transactional or roll back the credential write when saving state fails.
if err := store.SaveDeviceState(credstore.DeviceState{
internal/commands/auth.go:388
- The presence of a credential record does not establish that the user is logged in:
--with-tokenaccepts any non-empty string without validation, and stored refresh tokens can later be revoked. In both cases this command prints “Logged in.” even though token acquisition fails. Validate the session (for example by refreshing when no valid access token is cached), or report only that credentials are stored rather than asserting an authenticated status.
fmt.Println("Logged in.")
internal/commands/auth.go:214
- A successful OAuth token response is not required to include a refresh token. Saving such a response and printing “Login successful” creates a session that necessarily stops working when the access token expires. Check
token.RefreshTokenbefore persisting and return an actionable error if Auth0 did not issue one.
if err := store.SaveCredentials(credstore.Credentials{
RefreshToken: token.RefreshToken,
AccessToken: token.AccessToken,
AccessTokenExpiry: token.Expiry,
}); err != nil {
internal/commands/auth.go:40
- This comment is inaccurate:
auth statusnever reads identity claims; these scopes are used only for the post-login “Logged in as …” message at lines 227-230. Describe that actual use so future scope changes do not rely on a nonexistent status dependency.
// scopes requested during the device code flow. offline_access is required
// to receive a refresh token; the rest identify the user for `auth status`.
Extract persistLogin and loadStoredCredentials helpers to eliminate the two Go clones MegaLinter's jscpd check flagged in internal/commands/auth.go: the duplicated SaveCredentials + SaveDeviceState pairs in the --with-token and device-code login paths, and the duplicated LoadCredentials/ErrNotFound loading pattern shared by the token and status commands. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/commands/auth.go:299
- If the credential write succeeds but the state write fails (disk full, permissions, or a partial state-file write), the command reports login failure after already replacing the credentials. The new refresh token is then paired with stale environment metadata—or no metadata—so later refreshes fail, and a previously valid login may have been destroyed. Make these writes transactional from the caller’s perspective by restoring/deleting the newly written credentials when state persistence fails (and preserving any prior login as appropriate).
if err := store.SaveCredentials(creds); err != nil {
return fmt.Errorf("save credentials: %w", err)
}
if err := store.SaveDeviceState(state); err != nil {
return fmt.Errorf("save device state: %w", err)
internal/commands/auth.go:209
- A successful OAuth token response is not guaranteed to contain a refresh token, even when
offline_accessis requested (for example, an overridden audience may have offline access disabled). This path still persists the response and prints “Login successful,” so authentication works only until the cached access token expires, after whichauth tokencan never refresh it. Require a non-emptytoken.RefreshTokenbefore persisting the login and return an actionable configuration/login error otherwise.
RefreshToken: token.RefreshToken,
Document a Go toolchain upgrade policy in the Contributing Guidelines: freely bump go.mod's go directive to the latest patch release, but only bump the minor version when the user explicitly asks for it and it has been validated against the Go version MegaLinter itself bundles -- MegaLinter runs several linters (e.g. golangci-lint) against its own bundled Go version, and a go.mod directive newer than that bundled version breaks those checks. Includes the concrete steps to look up MegaLinter's bundled Go version from its pinned flavor tag, and a one-liner using the go.dev/dl JSON feed to find the latest patch release for the minor version currently pinned in go.mod. Downgrade go.mod's go directive from 1.26.5 to 1.25.14 (the latest 1.25.x patch release). Verified against .github/workflows/mega-linter.yml, which pins oxsecurity/megalinter's go flavor to v9.6.0; that flavor's Dockerfile bundles Go 1.26.3 (GO_ALPINE_VERSION=1.26.3-r0), so the previous 1.26.5 directive was already newer than MegaLinter's own Go toolchain -- exactly the failure mode this policy exists to prevent. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
Comparing Credentials via struct-level != breaks whenever AccessTokenExpiry's time.Time round-trips through JSON in an environment where time.Parse assigns it a different *Location than the original value, even though both represent the identical instant (time.Time.Equal returns true). This only reproduced under TZ=UTC (GitHub Actions' default), not the author's local PDT environment, which is why it passed locally but failed in CI. Compare AccessTokenExpiry with Equal instead of as part of the struct-level comparison. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
keyring.Open probes systemBackends in priority order and silently uses whichever one currently opens, so the same machine can land on a different backend across invocations (e.g. Secret Service reachable in one shell session but not another, falling back to pass). Since state.json is shared across all backends, that can silently pair a refresh token from one backend with IdP/environment metadata written by another. Add --backend (credstore.Options.Backend / DeviceState.Backend) to pin keyring.Open to a single backend. Once a login has pinned one, later commands must pass the same --backend value or fail loudly, mirroring the existing --insecure-storage mismatch guard rather than silently trusting stale state. An unpinned login (the previous, still-default behavior) can't be protected the same way: there's no recorded backend to check a later invocation against. This mirrors aws-vault's own approach: as the most established consumer of the same keyring library, it requires --backend/ AWS_VAULT_BACKEND to pin a backend explicitly rather than trusting keyring.Open's own auto-detection to be stable across invocations. Also: - Show the pinned backend (from persisted state, not just the current flag) as its own line in `lfx auth status`, and align all of its output labels to a common column. - Add a repo-level .jscpd.json excluding *_test.go from duplication scanning, needed once MegaLinter's own default (which excludes **/*.yaml, **/*.md, etc. but not *_test.go) is replaced by ours; otherwise structurally-similar-but-distinct negative-path test cases (e.g. the new backend-pin-mismatch test alongside the existing insecure-storage/domain-mismatch ones) trip jscpd's 0% duplication threshold. The existing shared test helpers (newTestCommand, newInsecureStore, fakeIDToken) already cover the reusable parts; forcing further abstraction of each test's short, intentionally-similar arrange/assert block would hurt readability for no real benefit. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
go get -u ./... && go mod tidy, per contributing guidelines. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
internal/commands/auth.go:435
- This cached-token fast path runs before
loadDeviceStateForBackend, so it bypasses the newly required backend check. For example, if a login is pinned to Keychain but auto-detection later openspasscontaining an older still-valid token,lfx auth tokensilently prints that token; the same mismatch is rejected only after it expires. Validate the stored state before either returning a cached token or refreshing it.
if creds.ValidAccessToken() {
fmt.Println(creds.AccessToken)
return nil
internal/commands/auth.go:552
- Credentials are deleted before the persisted backend is checked. If a login is pinned to Keychain but this invocation opens
pass(explicitly or through changed auto-detection), this deletes the wrong entry, leaves the actual Keychain credentials intact, and still prints “Logged out.” Load and validateDeviceStatebefore this destructive operation; on a mismatch, return the existing actionable backend hint instead of reporting success.
if err := store.DeleteCredentials(); err != nil {
return fmt.Errorf("delete credentials: %w", err)
}
- New validated --backend against the cross-platform systemBackends list rather than AvailableBackends() (the subset actually compiled into this binary for the current OS). A value like "keychain" on Linux therefore passed validation only to fail later with a generic keyring-open error, and the "available" list in that validation error didn't match what `lfx auth backends` reports. Validate against AvailableBackends() instead. - --backend's help text said it was "ignored with --insecure-storage", but credStoreFromCommand rejects that combination outright. Describe them as mutually exclusive instead, matching the actual behavior. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
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.
internal/commands/auth.go:445
- The cached-token fast path returns before the persisted backend is validated. A login pinned with
--backendcan therefore be invoked with that flag omitted or changed while its access token is still valid, potentially printing a token from an auto-selected/different credential store. Load and validate the device state before this fast path.
This issue also appears on line 550 of the same file.
if creds.ValidAccessToken() {
fmt.Println(creds.AccessToken)
return nil
internal/commands/auth.go:552
- Credentials are deleted before the pinned backend in
state.jsonis checked. For example, after a--backend=passlogin, omitting the flag can auto-open Secret Service, delete that store's entry, leave the actual pass login intact, and still print “Logged out.” Load and validate the state before mutating credentials, and reject a pinned-backend mismatch (or explicitly report that only the selected backend was cleared).
if err := store.DeleteCredentials(); err != nil {
return fmt.Errorf("delete credentials: %w", err)
}
internal/commands/auth.go:72
- This help text says
--backendis ignored with--insecure-storage, butcredStoreFromCommandrejects that combination. Update the user-facing contract to match the implemented mutual exclusion.
&cli.StringFlag{
Name: backendFlagName,
Usage: "Pin credential storage to a specific system backend (see `lfx auth backends`); mutually exclusive with --insecure-storage",
internal/credstore/credstore.go:299
- The “available” list is built from every supported cross-platform backend, not the current OS's
AvailableBackends. Consequently an invalid value on Linux can advertisekeychainandwincredas available even thoughkeyring.Opencannot use them there. Build this error's list fromAvailableBackendsand distinguish recognized-but-unavailable backends.
path: filepath.Join(stateDir, insecureCredentialsFileName),
}
} else {
allowedBackends := systemBackends
if opts.Backend != "" {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
internal/commands/auth.go:552
- Credentials are deleted before the pinned backend is checked. When
--backendis omitted or differs but auto-detection opens the pinned store, this removes that login, preserves its state as “mismatched,” and still printsLogged out.; if another store opens, the active pinned login can remain while the command still reports success. Reject a same-storage-mode pinned-backend mismatch before deleting anything.
if err := store.DeleteCredentials(); err != nil {
return fmt.Errorf("delete credentials: %w", err)
}
|
Hi @emsearcy 👋 — thanks for the fast, substantial turnaround on this. You addressed the blocking test gap head-on and the scope actually grew in a good direction. Nice iteration. 👏 Nice work this round
Revision tracking (prior round)
Issue summary (open items, carried + new)
Bot reconciliation
Decision🔴 Needs changes before approval — no blockers remain and the security posture is solid, but there are 3 open minors (two of them quick doc/message fixes, one a small UX correction on the new |
dealako
left a comment
There was a problem hiding this comment.
Follow-up review . see the summary comment for full revision tracking and decision rationale. Two inline minors below; no blockers.
newAuthTokenCommand's ValidAccessToken fast path returned a cached access token before loadDeviceStateForBackend ran, so omitting a pinned --backend could still auto-open some other backend and print its cached token, bypassing the pin. Move the device-state validation ahead of the cache check so it always runs first. Also fix a stray "Ignored when --insecure-storage is set" doc comment on backendFlagName that should have been updated alongside the --backend/--insecure-storage mutual-exclusion fix in fa9c0ec. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
stateMatchesInvocation returns false for either an --insecure-storage mismatch or a --backend pin mismatch, but the advisory notes in `auth status` and `auth logout` rendered the reason using only backendDescription(state.Insecure), which only ever describes the Insecure case. For a pinned-backend mismatch (Insecure actually matches), the note misleadingly named the *current* invocation's own backend and never mentioned the pinned backend or the --backend flag needed to match it. Add stateMismatchReason, mirroring loadDeviceStateForBackend's existing backend-specific error message, and use it in both notes. Assisted-by: github-copilot:claude-sonnet-5 Signed-off-by: Eric Searcy <eric@linuxfoundation.org>
| return err | ||
| } | ||
|
|
||
| if err := store.DeleteCredentials(); err != nil { |
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 (1)
internal/commands/auth.go:599
Logged out.can be printed while the pinned login remains active. For example, after login with--backend=pass, running logout without that flag may auto-open Secret Service;DeleteCredentialsremoves nothing there, the mismatch branch preserves thepassstate, and this line still reports success even thoughlfx auth token --backend=passcontinues to work. Validate a pinned backend before deleting credentials and return the matching-flag error, or otherwise avoid reporting a successful logout when the stored login was not removed.
fmt.Println("Logged out.")
Summary
Implements
lfx auth login,lfx auth token,lfx auth status, andlfx auth logouton top ofgolang.org/x/oauth2's device authorization grant support (Config.DeviceAuth/DeviceAccessToken) and refresh-tokenTokenSource, targeting the static per-environment Auth0 client IDs provisioned in LFXV2-2513.internal/commands/environment.go: resolves--envto its IdP domain and compiled-in client ID.internal/commands/auth.go: reallogin(interactive device code flow, or--with-tokenfor headless/CI use),token(cached-token fast path, refresh-and-recache on expiry, clear error message on invalid/expired refresh token),status,logout, andbackendsimplementations. Callsgolang.org/x/oauth2'sConfig.DeviceAuth/DeviceAccessTokenand refresh-tokenTokenSourcedirectly (no wrapper package/types in between) for the device code request, poll, and refresh-token exchange.internal/credstore: extendsDeviceStatewithenvironment/audience(needed to rebuild the same IdP client on refresh),insecure(guards against mixing state between the keychain and--insecure-storagebackends), and addsDeleteDeviceStatefor logout. Also addsAvailableBackends, which reports the system credential-store backends compiled into the binary for the current OS.lfx auth backends: lists the system credential-store backend(s) available on this OS (e.g. macOS Keychain,pass, Linux Secret Service/KWallet, Windows Credential Manager), in the priority orderlfx auth logintries them. This only reflects what's compiled in per-OS build tags, not whether a given backend is actually usable at runtime (e.g. no D-Bus session for Secret Service). User-facing "system keychain" wording elsewhere was renamed to the more accurate, OS-agnostic "system backend" to match..cspell.jsonwordlist additions/fixes (nolint,containedctx,rundll, plus suppressing spellcheck on the opaque Auth0 client ID literals).Notably NOT implemented: persistent "device ID"
The epic's background/design notes called for a "Device ID persistence in
~/.local/state/lfx-cli/," modeled on an assumedghCLI precedent. I checkedgh's actual behavior directly:~/.local/state/gh/device-idexists, but it's generated byinternal/telemetry.getOrCreateDeviceIDingithub.com/cli/cli— an anonymous telemetry identifier, not something used ingh's own OAuth device flow or credential storage. Auth0's device authorization grant has no concept of a device ID either. Since the LFX CLI has no telemetry pipeline in scope here, there's nothing for one to do; left out ofcredstore.DeviceStatewith a comment explaining the rationale. See ticket comments on LFXV2-2515/LFXV2-2516 for the full writeup.Related tickets
lfx auth login)lfx auth tokencommandTesting
go build ./...,go vet ./...,golangci-lint run,revive ./...all pass cleango test ./...) covercredstore's Save/Load/Delete round-trip andValidAccessToken,persistLogin's success/rollback paths,loadDeviceStateForBackend's backend/IdP-domain mismatch guards, andidentityFromIDToken's claim-selection logiclogin/status/token/logoutcycle end-to-end against thedevandprodAuth0 tenants (--env development/--env prod), including the interactive device code flow, cached-token fast path, and refresh-token exchangesso.linuxfoundation.org, notlinuxfoundation.auth0.com(seeauth0-terraform's ownauth0_domainvariable) — using the wrong domain surfaced as an Auth0invalid_request: Missing required parameter: response_typeerror and a login that never completed🤖 Generated with GitHub Copilot (via OpenCode)