Skip to content

feat(github-oauth): add github oauth lease backend#464

Merged
jdx merged 4 commits intomainfrom
codex/add-github-oauth-lease
May 6, 2026
Merged

feat(github-oauth): add github oauth lease backend#464
jdx merged 4 commits intomainfrom
codex/add-github-oauth-lease

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented May 6, 2026

Summary

Adds a github-oauth lease backend that mirrors the useful parts of ghtkn inside Fnox: GitHub App user access tokens via OAuth device flow, optional browser opening, OS keyring caching, refresh-token reuse, and injection as GITHUB_TOKEN or a configured env var.

A key benefit is that github-oauth only needs the GitHub App client ID. It does not require an app private key or app client secret, so the lease config can be safely shared across a team. The existing github-app backend remains the installation-token path and still uses an app private key, not the app client secret.

Also adds docs for the backend, adds a github-app tip suggesting github-oauth for local/user-attributed workflows, registers it in the lease backend table, regenerates the public config schema, and covers the device-flow path with a mock GitHub OAuth bats test.

Validation

  • cargo fmt --check
  • cargo check -p fnox-core
  • cargo build
  • ./test/bats/bin/bats test/lease_github_oauth.bats

Note: mise run build and the hk pre-commit hook both blocked while installing vault@2.0.0; the commit was made with HK=0 after the checks above passed.

This PR description was generated by Codex.


Note

Medium Risk
Adds a new GitHub OAuth device-flow backend that vends and caches user access tokens (including optional refresh-token reuse and OS keyring storage), which affects credential acquisition paths and introduces new external API interactions.

Overview
Adds a new github-oauth lease backend that obtains GitHub App user access tokens via OAuth device flow, optionally opens the verification URL in a browser, and caches/refreshes tokens via the OS keyring before injecting the token into a configurable env var (default GITHUB_TOKEN).

Wires the backend into LeaseBackendConfig (defaults for scope/auth+api base URLs, config schema updates), adds user-facing docs (including a tip in github-app docs and updating the supported backends table), and introduces a bats test that exercises the device/refresh flows against a mock HTTP server.

Reviewed by Cursor Bugbot for commit 29f9d75. Bugbot is set up for automated code reviews on this repo. Configure here.

@jdx jdx changed the title [codex] add github oauth lease backend feat(github-oauth): add github oauth lease backend May 6, 2026
@jdx jdx marked this pull request as ready for review May 6, 2026 13:53
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new github-oauth lease backend, enabling GitHub App user access tokens via the OAuth device flow. The implementation includes the core backend logic, configuration support, documentation, and comprehensive tests. My review identified three actionable improvements: replacing the unstable let_chains syntax with nested if let statements for stable Rust compatibility, improving the keyring cache key to prevent collisions when using multiple GitHub instances, and optimizing the polling loop to perform an immediate initial request rather than sleeping first.

Comment on lines +322 to +327
if let Some(cached) = self.read_cached_token()
&& let Some(refreshed) = self.refresh_access_token(&cached).await?
{
self.write_cached_token(&refreshed);
return Ok(refreshed);
}
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.

high

This code uses the let_chains feature (if let ... && let ...), which is currently unstable in Rust. Unless the project explicitly requires a nightly toolchain, it is recommended to use nested if let statements to maintain compatibility with the stable Rust compiler.

Suggested change
if let Some(cached) = self.read_cached_token()
&& let Some(refreshed) = self.refresh_access_token(&cached).await?
{
self.write_cached_token(&refreshed);
return Ok(refreshed);
}
if let Some(cached) = self.read_cached_token() {
if let Some(refreshed) = self.refresh_access_token(&cached).await? {
self.write_cached_token(&refreshed);
return Ok(refreshed);
}
}

}

fn cache_key(&self) -> String {
let hash = blake3::hash(format!("{}|{}", self.client_id, self.scope).as_bytes());
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.

medium

The cache key for the OS keyring currently only includes the client_id and scope. If a user configures multiple GitHub instances (e.g., public GitHub and a GitHub Enterprise Server) using the same client ID and scope, the tokens will collide in the keyring. Including the auth_base and api_base in the hash ensures that tokens are cached uniquely per endpoint.

Suggested change
let hash = blake3::hash(format!("{}|{}", self.client_id, self.scope).as_bytes());
let hash = blake3::hash(format!("{}|{}|{}|{}", self.client_id, self.scope, self.auth_base, self.api_base).as_bytes());

Comment on lines +173 to +178
loop {
if chrono::Utc::now() >= deadline {
return Err(auth_failed("Device authorization expired".to_string()));
}

tokio::time::sleep(Duration::from_secs(interval)).await;
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.

medium

The polling loop currently sleeps before making the first request. This introduces an unnecessary delay (e.g., 5 seconds) even if the user has already authorized the device in their browser. It is better to perform the first poll immediately and then sleep only if the authorization is still pending.

@jdx jdx force-pushed the codex/add-github-oauth-lease branch from 3ad12c0 to 2d88dbf Compare May 6, 2026 13:55
@jdx jdx force-pushed the codex/add-github-oauth-lease branch from 2d88dbf to f18158a Compare May 6, 2026 13:56
Comment thread crates/fnox-core/src/lease_backends/github_oauth.rs Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 6, 2026

Greptile Summary

Adds a github-oauth lease backend that issues GitHub App user access tokens via the OAuth device flow, caches them in the OS keyring, and rotates them with refresh tokens before falling back to a fresh device flow. Also registers the backend in the config enum, regenerates the public JSON schema, adds documentation, and covers the flow with a Bats mock-server test.

  • New backend (github_oauth.rs): implements LeaseBackend, handles device-code polling with RFC 8628 back-off, token/refresh caching in the OS keyring, non-blocking browser open via spawn_blocking, and graceful debug-logged fallback when the refresh token is rejected.
  • Config/schema/docs (mod.rs, schema.json, leases.md, github-oauth.md): all match arms are updated exhaustively; the new variant is wired into check_prerequisites, required_env_vars, is_output_var, consumed_env_vars, and the backend factory.

Confidence Score: 5/5

Safe to merge. The new backend is self-contained, all match arms are exhaustively updated, and the three concerns raised in previous review rounds have been properly addressed.

The device-flow implementation correctly follows RFC 8628 (slow_down back-off, deadline enforcement, error discrimination). Token/refresh caching is gated behind keyring_cache, browser opens are non-blocking via spawn_blocking, and refresh failures fall back gracefully with a debug log. Config wiring in mod.rs is complete and consistent. The only note is the implicit URL derivation in device_auth_base, which works correctly for standard GitHub and GHE paths but could silently misbehave for unusual custom auth_base values.

No files require special attention for a standard merge. Users setting a non-standard auth_base (without a trailing /oauth segment) for GitHub Enterprise should be aware of how the device-code endpoint URL is derived.

Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(github-oauth): address pr feedback" | Re-trigger Greptile

Comment thread crates/fnox-core/src/lease_backends/github_oauth.rs Outdated
Comment thread crates/fnox-core/src/lease_backends/github_oauth.rs
Comment thread crates/fnox-core/src/lease_backends/github_oauth.rs
Comment thread crates/fnox-core/src/lease_backends/github_oauth.rs Outdated
@jdx jdx merged commit 5766b38 into main May 6, 2026
16 checks passed
@jdx jdx deleted the codex/add-github-oauth-lease branch May 6, 2026 18:48
jdx pushed a commit that referenced this pull request May 6, 2026
### 🚀 Features

- **(github-oauth)** add github oauth lease backend by
[@jdx](https://github.com/jdx) in
[#464](#464)

### 🐛 Bug Fixes

- **(ci)** de-duplicate sponsor section in release notes by
[@jdx](https://github.com/jdx) in
[#459](#459)

### 🔍 Other Changes

- **(ci)** use !cancelled() instead of always() for final job by
[@jdx](https://github.com/jdx) in
[#461](#461)
- set dev profile debug to 1 by [@jdx](https://github.com/jdx) in
[#462](#462)
fullerzz pushed a commit to fullerzz/fnox-py that referenced this pull request May 9, 2026
## Upstream release

Bumps bundled fnox binary from 1.23.1 to 1.24.0.

**Release**: https://github.com/jdx/fnox/releases/tag/v1.24.0

## Release notes

A focused release that adds a new `github-oauth` lease backend for
minting short-lived, user-attributed GitHub tokens via OAuth device flow
— without distributing an app private key.

## Added

**`github-oauth` lease backend**
([#464](jdx/fnox#464)) -- @jdx

A new lease type that creates GitHub App *user access tokens* using the
OAuth device flow and injects them as `GITHUB_TOKEN` (or a custom env
var) for the duration of `fnox exec`. It is the recommended option for
local development and user-attributed `gh` / GitHub API usage where you
want a short-lived token tied to the signed-in user instead of a
long-lived PAT in `fnox.toml`.

```toml
[leases.github]
type = "github-oauth"
client_id = "Iv1.yourgithubappclientid"
scope = "repo read:org workflow"
duration = "8h"
```

```sh
fnox exec -- gh pr list
```

On first run, fnox prints a verification URL and user code, optionally
opens the URL in your browser, and polls GitHub until you approve the
device prompt. Subsequent runs reuse the cached token until it expires.

Highlights of the backend:

- **Only the GitHub App client ID is required** — no app private key and
no client secret, so the lease config can be checked in and shared
across a team. (The existing `github-app` backend remains the right
choice for installation tokens in CI.)
- **OS keyring caching** of access and refresh tokens, keyed by client
id + scope + endpoints. Disable with `keyring_cache = false` to force
the device flow on every lease.
- **Refresh token reuse** when GitHub issues one — refreshes happen
transparently; if the refresh fails, fnox falls back to a fresh device
flow.
- **Configurable env var** via `env_var` (e.g. `"GH_TOKEN"`) and
configurable `auth_base` / `api_base` for GitHub Enterprise Server.
- **`open_browser`** controls whether fnox tries to launch the
verification URL automatically (uses `open` / `xdg-open` / `start`).

The supported-backends table in the leases guide is updated, and the
`github-app` docs now point local/user-attributed workflows at
`github-oauth`. See the [GitHub OAuth lease
docs](https://fnox.jdx.dev/leases/github-oauth) for the full reference.

**Full Changelog**:
jdx/fnox@v1.23.1...v1.24.0


## 💚 Sponsor fnox

fnox is maintained by [@jdx](https://github.com/jdx) under
[**en.dev**](https://en.dev) — a small independent studio building
developer tooling like [mise](https://mise.jdx.dev/),
[aube](https://aube.en.dev/), hk, and more. Keeping fnox secure,
maintained, and free is funded by sponsors.

If fnox is handling secrets or config for you or your team, please
consider [sponsoring at en.dev](https://en.dev). Sponsorships are what
let fnox stay independent and the project keep moving.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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