Skip to content

feat: native OIDC device-code auth — vmx_login() + auto-auth in vmx_client() (GEN-2332)#13

Merged
jburos merged 3 commits into
mainfrom
feature/gen-2332-native-oidc-auth
Jul 3, 2026
Merged

feat: native OIDC device-code auth — vmx_login() + auto-auth in vmx_client() (GEN-2332)#13
jburos merged 3 commits into
mainfrom
feature/gen-2332-native-oidc-auth

Conversation

@agentic-task-writer

@agentic-task-writer agentic-task-writer Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • vmx_login() runs the OIDC device-code flow (RFC 8628, via httr2::oauth_flow_device()) against the Authentik provider — public client, no secret/PKCE, offline_access scope 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, prompting vmx_login() only when there's no usable cached token.
  • Token cached as plain JSON at ~/.config/vmx/oidc-token.json — the same path and shape the vmx CLI 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).
  • Config from env vars matching the CLI: 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-cli provider; vmx-api accepts the Bearer token). Cache is shape/field-compatible with vmx-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 are openid profile email offline_access goauthentik.io/api.

Notes for review:

  • jsonlite promoted Suggests → Imports (used for the plain-JSON cache; already a transitive dep of httr2).
  • Existing PAT path is unchanged; explicit token= / VMX_API_TOKEN bypasses OIDC. The prior "no token" case now raises a vmx_auth_error naming both auth methods (same class as before).
  • Out of scope (tracked elsewhere): workspace env-var injection (GEN-2327), vmx-cli offline_access scope fix (GEN-2331).

Test plan

  • Device-code happy path (mocked provider) → CLI-shaped 0600 cache written
  • Silent refresh of an expired token; keeps a non-rotating refresh token
  • Cache round-trips across a fresh session (pod-restart persistence)
  • vmx_client() auto-auths from a valid cached token; PAT bypasses OIDC
  • No-auth error path (no PAT + no OIDC config; configured-but-no-cache non-interactive)
  • CI green (R-universe build/check)

⚠️ No R toolchain in the headless container — the testthat suite was written to httr2/testthat idioms but not run locally; it validates on CI (r-universe build/check on pull_request). Held for human merge per headless run policy.

🤖 Generated with Claude Code

Jacqueline Buros and others added 3 commits July 3, 2026 03:08
…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>
@generable generable deleted a comment from agentic-task-writer Bot Jul 3, 2026
@generable generable deleted a comment from agentic-task-writer Bot Jul 3, 2026
@jburos jburos merged commit 84a6f64 into main Jul 3, 2026
22 checks passed
@jburos jburos deleted the feature/gen-2332-native-oidc-auth branch July 3, 2026 05:14
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>
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.

1 participant