feat: native OIDC device-code auth — vmx_login() + auto-auth in vmx_client() (GEN-2332)#13
Merged
Merged
Conversation
…lient() (GEN-2332) Add native-R OIDC device-code authentication so vmxr authenticates to vmx-api without the Python CLI: - vmx_login() runs the RFC 8628 device-code flow via httr2::oauth_flow_device() against the Authentik provider (public client, no secret/PKCE, offline_access scope for a refresh token), resolving endpoints from the issuer discovery document. - vmx_client() auto-authenticates from the cached token when no PAT (VMX_API_TOKEN / token=) is set, refreshing silently and prompting vmx_login() only when there is no usable cached token. - Token cached as plain JSON at ~/.config/vmx/oidc-token.json — the same path/shape the vmx CLI uses (0600) — so one login serves both R and the CLI and survives a fresh R session / pod restart. - Config from VMX_OIDC_ISSUER / VMX_OIDC_CLIENT_ID / VMX_OIDC_SCOPES. Tests cover the device-code happy path, silent refresh (incl. keeping a non-rotating refresh token), cache round-trip across sessions, client auto-auth, and the no-auth error path. jsonlite promoted to Imports for the JSON cache. Docs: README OIDC section + vmx_login() man page. Validated end-to-end by the GEN-2330 spike. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…EN-2332) Address non-blocking review findings on PR #13: - .vmx_save_cached_token: tighten umask to 0177 for the write so the cache is 0600 from birth (no umask-default window), and use an unpredictable tempfile() name in the target dir instead of a fixed "<path>.tmp" (removes the symlink-swap target), with cleanup on error. - vmx_oidc_access_token: in the non-interactive path, surface why a silent refresh failed (revoked/expired refresh token, network) rather than collapsing every cause into the generic auth error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cache-permission assertion in test-oidc.R read file.info()$mode == "600" unconditionally, which fails on Windows: Windows has no POSIX permission bits, so file.info()$mode reflects only the read-only flag, not 0600. This turned the three Windows R-universe builds red while Linux/macOS/Wasm passed (209/209). Guard the mode check to .Platform$OS.type != "windows" — every portable assertion still runs on all platforms, and the 0600 owner-only check runs where POSIX modes are meaningful (the Linux home PVC that is the actual deployment target). No source change; the cache-hardening behaviour is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jburos
pushed a commit
that referenced
this pull request
Jul 3, 2026
Resolve the bearer token per request via a provider closure instead of freezing the OIDC access token at construction. A persistent `con <- vmx_client()` (the dominant R idiom) now re-reads the cache and silently refreshes the short-lived access token from the cached refresh token, so it keeps working across a full session instead of throwing vmx_auth_error minutes in. Also addresses the agreed PR #13 review nits: - Wrap .vmx_oidc_device_flow() failures (user-denied / code-expired / polling-exhausted) as vmx_auth_error so they stay in the vmx_error hierarchy. - Don't silently clobber a config-mismatched cache on interactive auto-login: warn before overwriting (the cache is shared with the CLI); non-interactive raises a message naming the mismatch. - Reword the "no usable cached token" message so it is accurate when a token exists but is expired-without-refresh or for another config. - Document VMX_OIDC_TOKEN_CACHE as a testing-only override. Tests: reused-client stale path (the structural gap), skew-boundary (valid token returned without refresh; within-skew refreshed), config-mismatch handling, and device-flow error wrapping. Co-authored-by: agentic-task-writer[bot] <agentic-task-writer[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vmx_login()runs the OIDC device-code flow (RFC 8628, viahttr2::oauth_flow_device()) against the Authentik provider — public client, no secret/PKCE,offline_accessscope for a refresh token; endpoints resolved from the issuer's.well-known/openid-configuration(not hardcoded).vmx_client()auto-authenticates from the cached token when no PAT (VMX_API_TOKEN/token=) is set — silently refreshing with the refresh token, promptingvmx_login()only when there's no usable cached token.~/.config/vmx/oidc-token.json— the same path and shape thevmxCLI writes (0600) — so one login serves both R and the terminal CLI, and the refresh token on the home PVC lets the session survive a fresh R process / pod restart (~30-day login).VMX_OIDC_ISSUER,VMX_OIDC_CLIENT_ID,VMX_OIDC_SCOPES.Closes GEN-2332
Context / design
Validated end-to-end by the GEN-2330 spike (native-R device-code works against the staging Authentik
vmx-cliprovider; vmx-api accepts the Bearer token). Cache is shape/field-compatible withvmx-services/clients/cli/src/vmx_cli/oidc.py(TokenSet):access_token,refresh_token,expires_at(absolute epoch),token_type,issuer(trailing-slash-stripped),client_id. Default scopes areopenid profile email offline_access goauthentik.io/api.Notes for review:
jsonlitepromoted Suggests → Imports (used for the plain-JSON cache; already a transitive dep of httr2).token=/VMX_API_TOKENbypasses OIDC. The prior "no token" case now raises avmx_auth_errornaming both auth methods (same class as before).offline_accessscope fix (GEN-2331).Test plan
0600cache writtenvmx_client()auto-auths from a valid cached token; PAT bypasses OIDCpull_request). Held for human merge per headless run policy.🤖 Generated with Claude Code