[](https://crates.io/crates/loopauth)
[](https://docs.rs/loopauth)
[](#license)
# loopauth
OAuth 2.0 Authorization Code + PKCE flow for CLI applications.
`loopauth` opens the user's browser to the authorization URL, spins up a short-lived loopback server to receive the redirect callback, exchanges the authorization code for tokens, and returns a `TokenSet` containing access and refresh tokens. Your application can then use those tokens (e.g., the ID token) to authenticate users against your own backend.
It is not a full OAuth 2.0 or OIDC library. Other grant types (client credentials, device flow, etc.), token introspection, and app-level authentication are out of scope.
Token storage and downstream identity consumption are intentionally out of scope; implement the `TokenStore` trait to provide your own persistence.
## Quick start
### OIDC auto-discovery
Provider URLs are fetched automatically via `.well-known/openid-configuration`:
```rust
use loopauth::{CliTokenClientBuilder, RequestScope, oidc::OpenIdConfiguration};
use url::Url;
let open_id_configuration = OpenIdConfiguration::fetch(
Url::parse("https://provider.example.com")?,
).await?;
// from_open_id_configuration automatically includes the openid scope
let client = CliTokenClientBuilder::from_open_id_configuration(&open_id_configuration)
.client_id("my-client-id")
.with_open_id_configuration_jwks_validator(&open_id_configuration)
.add_scopes([RequestScope::Email])
.build();
let tokens = client.run_authorization_flow().await?;
```
### Explicit URLs
```rust
use loopauth::{CliTokenClient, RequestScope};
use url::Url;
let client = CliTokenClient::builder()
.client_id("my-client-id")
.auth_url(Url::parse("https://provider.example.com/authorize")?)
.token_url(Url::parse("https://provider.example.com/token")?)
.with_openid_scope()
.without_jwks_validation() // or .jwks_validator(Box::new(my_validator))
.add_scopes([RequestScope::Email])
.build();
let tokens = client.run_authorization_flow().await?;
```
## HTTPS callbacks
Some OAuth providers (notably Slack) require `https://` redirect URIs, even for
localhost. loopauth supports HTTPS callbacks using locally-trusted TLS
certificates.
### How it works
By default, loopauth serves the callback over plain HTTP on `127.0.0.1`, which
is permitted by [RFC 8252 Section 7.3](https://datatracker.ietf.org/doc/html/rfc8252#section-7.3)
and accepted by most providers. When a provider requires HTTPS, call
`.use_https_with(cert)` on the builder to serve over TLS with a trusted
certificate, or `.use_https()` for a self-signed fallback (development only).
The recommended approach uses [mkcert](https://github.com/FiloSottile/mkcert) to
create a locally-trusted certificate authority. Certificates signed by this CA
are trusted by all browsers on the machine, so the OAuth redirect completes
seamlessly with no certificate warnings.
### Who does what
There are three roles in the chain:
| **loopauth** (this crate) | Generates/loads certs, serves HTTPS, provides setup guide text |
| **CLI author** (your code) | Provides a config directory path, calls `ensure_localhost()` or `from_pem_files()` |
| **End user** (runs the CLI) | One-time: installs `mkcert` and runs `mkcert -install` |
### End-user setup (one-time)
The end user needs to do this once per machine. Both commands can be run from
any directory; they are global operations:
```sh
# 1. Install mkcert (https://github.com/FiloSottile/mkcert#installation)
# macOS: brew install mkcert
# Other platforms: see the link above
# 2. Create and install a local CA (may prompt for password).
# This adds a root certificate to the system trust store so browsers
# accept localhost certificates signed by it. It does not affect other
# machines or network traffic.
mkcert -install
```
That's it; there is no step 3. The end user does **not** need to run `mkcert`
to generate certificate files. `TlsCertificate::ensure_localhost()` handles
certificate generation automatically on first run, storing the files in your
app's config directory.
If `mkcert -install` has already been run (e.g. for another tool), the end user
only needs step 1 (installing the `mkcert` binary). Generating multiple
localhost certificates is fine; each is independently valid, all signed by the
same local CA.
### CLI author integration
Point `ensure_localhost` at your app's config directory. It handles everything:
- **First run:** creates the directory (if needed), shells out to `mkcert` to
generate `localhost-cert.pem` and `localhost-key.pem`, sets file permissions,
and loads the result.
- **Subsequent runs:** loads the existing cert files from disk. No `mkcert`
invocation occurs.
The cert files are self-contained and not tied to the directory where `mkcert`
was invoked; `ensure_localhost` manages the paths internally.
```rust
use loopauth::{CliTokenClient, TlsCertificate};
let cert = TlsCertificate::ensure_localhost(
config_dir.join("tls"),
)?;
let client = CliTokenClient::builder()
.client_id("my-client-id")
.auth_url(auth_url)
.token_url(token_url)
.use_https_with(cert)
.build();
let tokens = client.run_authorization_flow().await?;
```
If the end user hasn't installed `mkcert`, `ensure_localhost` returns
`TlsCertificateError::MkcertNotFound`. If the local CA isn't set up
(`mkcert -install` was never run), `TlsCertificateError::MkcertFailed` tells
them what to do.
For full control over cert loading (e.g. custom file paths or embedding certs),
use `TlsCertificate::from_pem_files()` or `TlsCertificate::from_pem()` instead.
### Providing setup instructions to end users
Two `&'static str` constants provide ready-made `mkcert` instructions for end
users:
- `TlsCertificate::SETUP_GUIDE_MANAGED`: covers only `mkcert` installation and
CA trust (steps 1-2). **Use this when your CLI manages cert generation
automatically via `ensure_localhost`.**
- `TlsCertificate::SETUP_GUIDE`: full four-step guide including manual cert
generation and file handoff.
```rust
// When using ensure_localhost (recommended):
println!("{}", loopauth::TlsCertificate::SETUP_GUIDE_MANAGED);
// When using from_pem_files (manual mode):
println!("{}", loopauth::TlsCertificate::SETUP_GUIDE);
```
### Self-signed fallback
For development or testing, `.use_https()` (without a certificate) generates an
ephemeral self-signed certificate. Browsers will display a certificate warning
that the user must click through for the callback to complete. This is not
recommended for end-user-facing flows.
### Provider-specific notes
**Slack**: Requires HTTPS redirect URIs. Register
`https://127.0.0.1:<port>/callback` in your Slack app's Redirect URLs. Use
`.require_port(8443)` on the builder to pin the port so it matches the
registered URL. The callback path `/callback` is not configurable.
## Features
- PKCE ([RFC 7636])
- OIDC discovery
- JWKS validation
- Token refresh
- HTTPS callbacks with locally-trusted certificates
- Configurable token storage
- Configurable browser open behavior
- Configurable HTML success/error pages
[RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636
## Examples
The `examples/` directory contains runnable demos. Each example's source file documents its required and optional environment variables.
| [`auth.rs`](examples/auth.rs) | Manual configuration (explicit auth + token URLs) |
| [`auth_discovery.rs`](examples/auth_discovery.rs) | OIDC discovery-based setup |
| [`auth_https.rs`](examples/auth_https.rs) | HTTPS callback with `mkcert` certificates (e.g. for Slack) |
| [`jwks_demo.rs`](examples/jwks_demo.rs) | JWKS JWT validation demo |
| [`refresh_demo.rs`](examples/refresh_demo.rs) | Token refresh flow |
Some examples require environment variables (provider credentials, URLs). Review the source file for each example before running it.
```sh
cargo run --example auth
cargo run --example auth_discovery
cargo run --example auth_https # set LOOPAUTH_TLS_DIR or LOOPAUTH_CERT_FILE + KEY_FILE
cargo run --example auth_https -- --setup-guide # print mkcert setup instructions
cargo run --example jwks_demo
cargo run --example refresh_demo
```
## Local development
You'll need to install [just](https://just.systems/).
```sh
just setup # install required tools (cargo-nextest, cargo-watch, cargo-llvm-cov)
just test # full test suite
just docs # open API docs in browser
```
## License
This crate is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
- MIT license ([LICENSE-MIT](LICENSE-MIT))
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))