Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ path = "src/lib.rs"
[dependencies]
async-trait = "0.1.89"
base64 = "0.22.1"
keyring = { version = "3.6.1", optional = true }
open = { version = "5.3.2", optional = true }
rand = { version = "0.9", optional = true }
sha2 = { version = "0.10.9", optional = true }
url = { version = "2.5.4", optional = true }
zeroize = { version = "1.8", optional = true, features = ["derive"] }
chrono = { version = "0.4.42", default-features = false, features = ["clock", "serde"] }
clap = { version = "4.5.53", features = ["derive", "std", "string"] }
jmespath = "0.5.0"
Expand All @@ -28,9 +34,12 @@ schemars = { version = "1.2.1", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["fs", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
tokio = { version = "1.48.0", features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync", "time"] }
tracing = "0.1.43"

[features]
pkce-auth = ["dep:keyring", "dep:open", "dep:rand", "dep:sha2", "dep:url", "dep:zeroize"]

[dev-dependencies]
pretty_assertions = "1.4.1"
tempfile = "3.23.0"
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# cli-engine

`cli-engine` is a Rust library for building consistent, domain-oriented CLI applications.
It provides shared framework pieces for command registration, authentication, authorization hooks,
middleware, structured output, schemas, guides, search, command tree rendering, and HTTP transport.
`cli-engine` is a Rust library for building consistent, domain-oriented CLI applications. It provides shared framework pieces for command registration, authentication, authorization hooks, middleware, structured output, schemas, guides, search, command tree rendering, and HTTP transport.

Consumer CLIs bring their own binary entrypoint and domain modules. `cli-engine` owns the common
execution pipeline so teams can add commands by copying a nearby command and filling in the
Expand Down Expand Up @@ -83,6 +81,12 @@ manifest that produces the same `(path, bytes)` pairs.
- [Basic example](examples/basic.rs)
- [Agent instructions](AGENTS.md)

## Cargo Features

| Feature | Description |
| --- | --- |
| `pkce-auth` | Enables `auth::pkce::PkceAuthProvider`, a built-in browser-based OAuth 2.0 PKCE flow with system keychain storage via the `keyring` crate. Adds optional dependencies: `keyring`, `open`, `rand`, `sha2`, `url`. |

## Verification

```sh
Expand Down
59 changes: 59 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,65 @@ For compatibility with existing provider binaries, `ExecProvider` also sends `re
value as `env`, accepts credential responses containing `realm`, and can fall back to the
`list-realms` action when `list-environments` is not available.

## PkceAuthProvider

`PkceAuthProvider` is a built-in OAuth 2.0 Authorization Code + PKCE provider (RFC 7636). It
requires the `pkce-auth` Cargo feature.

```toml
[dependencies]
cli-engine = { path = "...", features = ["pkce-auth"] }
```

The provider manages the full browser-based login flow:

1. Generates a random PKCE code verifier and SHA-256 challenge.
2. Starts a local TCP server on `127.0.0.1` to receive the OAuth callback.
3. Opens the system browser to the authorization endpoint.
4. Exchanges the returned code for tokens at the token endpoint.
5. Stores tokens in the system keychain via the `keyring` crate (macOS Keychain, Linux Secret
Service, or the platform's encrypted file fallback).
6. Refreshes expired tokens automatically using the stored refresh token.

```rust
use std::sync::Arc;
use cli_engine::{CliConfig, auth::pkce::PkceAuthProvider};

let provider = Arc::new(
PkceAuthProvider::new(
"primary",
"https://auth.example.com/oauth/authorize",
"https://auth.example.com/oauth/token",
"my-client-id",
&["openid", "profile"],
)
.with_app_id("my-cli"), // keychain service prefix
);

let config = CliConfig::new("my-cli", "My CLI", "my-cli")
.with_default_auth_provider("primary")
.with_auth_provider(provider);
```

Tokens are stored per `(app_id, provider_name, env)` tuple. The in-process cache avoids
redundant keychain reads. A 30-second expiry buffer triggers proactive refresh.

The redirect port defaults to `7443`. Override it with `PkceAuthProvider::with_redirect_port`.

### Environment Variable Overrides

At runtime, the provider checks for environment variable overrides before using its compiled-in
values. The prefix is the provider name uppercased with hyphens replaced by underscores:

| Variable | Purpose |
| --- | --- |
| `<PREFIX>_OAUTH_CLIENT_ID` | OAuth client ID. |
| `<PREFIX>_OAUTH_AUTH_URL` | Authorization endpoint URL. |
| `<PREFIX>_OAUTH_TOKEN_URL` | Token endpoint URL. |

For a provider named `"primary"`, the variables are `PRIMARY_OAUTH_CLIENT_ID`,
`PRIMARY_OAUTH_AUTH_URL`, and `PRIMARY_OAUTH_TOKEN_URL`.

## Dispatcher

[`Dispatcher`](../src/auth/dispatcher.rs) routes auth calls by provider name:
Expand Down
Loading
Loading