CLI to manage OAuth tokens
- Features
- Coverage
- Installation
- Configuration
- Usage
- Alternatives
- AI disclosure
- License
- Social
- Contributing
- Sponsoring
- Account discovery wizard: run bare, it finds the OAuth 2.0 grants reachable for an email address and prints a ready-to-append account configuration.
- Dynamic client registration: register a public client on the spot, with no provider console, when the provider advertises it.
- Authorization code grant: sign in through the browser, with a built-in redirection server that captures the callback.
- Manual flow completion: finish a flow by hand, from the URL your browser was sent to, when the redirection server cannot bind.
- Token refresh: renew an expired access token from its refresh token, on demand or automatically when reading it.
- PKCE: enabled with the S256 method by default, following the OAuth 2.1 posture; opt out for servers that reject it.
- Extra authorization parameters: forward provider-specific knobs (Google offline access, login hints, resource indicators) verbatim.
- Token storage: read and write tokens through your own shell commands, wiring into any credential manager.
- Hooks: run a shell command or raise a system notification on token issuance and refresh, split by outcome.
- JSON output: switch discovery and token commands to machine-readable output for scripts.
- Full standard, blocking client with TLS support:
- Rustls with ring crypto (requires
rustls-ringfeature, enabled by default) - Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Rustls with ring crypto (requires
Tip
Ortie uses cargo features to gate optional functionality; the default set is declared in Cargo.toml.
| RFC | What is covered |
|---|---|
| 6749 | The OAuth 2.0 framework: authorization code grant, access token issuance and refresh |
| 7636 | PKCE: the S256 and plain code challenges protecting the authorization code in transit |
| 7591 | Dynamic client registration: register a public client without any provider console |
| 8414 | Authorization server metadata: the wizard reads it to discover a provider's endpoints and registration endpoint |
Ortie can be installed with the installer:
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/ortie/master/install.sh | sudo shAs a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/ortie/master/install.sh | PREFIX=~/.local shThese commands install the latest binary from the GitHub releases section.
For a more up-to-date version than the latest release, check out the releases GitHub workflow and look for the Artifacts section. These pre-built binaries are built from the master branch.
Note
Such binaries are built with the default cargo features. If you need specific features, please use another installation method.
cargo install --locked ortieFor the git tip:
cargo install --locked --git https://github.com/pimalaya/ortie.gitIf you have the Flakes feature enabled:
nix profile install github:pimalaya/ortieOr run without installing:
nix run github:pimalaya/ortiegit clone https://github.com/pimalaya/ortie
cd ortie
nix runRun ortie with no argument to launch the discovery wizard: it asks for an email address (or a server or issuer URI), discovers the OAuth 2.0 services reachable for it, and prints a complete account fragment as valid TOML on stdout, its guidance embedded as comments. Ortie never writes your configuration itself; the prompts render on stderr, so appending the fragment is a one-liner, ortie >> ~/.config/ortie/config.toml.
A configuration is loaded from the first valid path among:
$XDG_CONFIG_HOME/ortie/config.toml$HOME/.config/ortie/config.toml$HOME/.ortierc
Override the path with -c <PATH> or ORTIE_CONFIG=<PATH>; multiple paths can be passed at once, separated by :. The first one is the base and the rest are deep-merged on top. The full field reference lives in config.sample.toml; ready-made per-provider blocks follow below.
You may also need a registered OAuth 2.0 application. The wizard offers three ways, most preferred first: dynamic registration when your provider advertises it (Fastmail does), a public application (Thunderbird credentials cover most consumer providers), or your own registration. Public Thunderbird credentials for various providers are listed at github.com/mozilla.
Ready-made configuration blocks for common providers follow. The discovery wizard fills most of these in for you; they are kept here for manual setups and for Microsoft Graph, which the wizard does not cover. Drop the relevant block under your [accounts.<name>] table and fill in the client credentials.
endpoints.authorization = "https://accounts.google.com/o/oauth2/v2/auth"
endpoints.token = "https://oauth2.googleapis.com/token"
scopes = ["https://www.googleapis.com/auth/carddav", "https://mail.google.com"]
extras.access_type = "offline"Use these current endpoints, not the legacy o/oauth2/auth / www.googleapis.com/oauth2/v3/token pair, which Google can reject at consent with "This app is blocked"; the discovery wizard already fills the current ones. Gmail and CardDAV are sensitive scopes, so an unverified own application only works for accounts listed as test users on its OAuth consent screen; the public Thunderbird application below is verified.
Public Thunderbird application:
client-id = "406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com"
client-secret.raw = "kSmqreRr0qwBWJgbf5Y-PjSU"
endpoints.redirection = "http://localhost"For your own application, set client-id and client-secret.raw to your registered values.
endpoints.authorization = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
endpoints.token = "https://login.microsoftonline.com/common/oauth2/v2.0/token"Public Thunderbird application:
client-id = "9e5f94bc-e8a4-4e73-b8be-63364c29d753"
endpoints.redirection = "https://localhost"The Thunderbird application above is registered for Outlook IMAP / SMTP, not for the Graph API. To mint Graph tokens (for example Himalaya's msgraph backend), request Graph scopes from a client registered for Graph:
endpoints.authorization = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
endpoints.token = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
scopes = ["https://graph.microsoft.com/User.Read", "https://graph.microsoft.com/Mail.ReadWrite", "https://graph.microsoft.com/Mail.Send", "offline_access"]Public Microsoft Graph PowerShell application:
client-id = "14d82eec-204b-4c2f-b7e8-296a70dab67e"
endpoints.redirection = "http://localhost"Work or school (Entra ID) accounts receive a JWT the Graph API accepts; personal Microsoft accounts may be issued an opaque token the API rejects with InvalidAuthenticationToken, so prefer a work or school account, or your own registered application.
Fastmail advertises RFC 7591 dynamic registration, so bare ortie can register a client for you. Two Fastmail specifics the wizard does not yet fill into the fragment, so add them by hand:
- RFC 8707 resource: Fastmail's authorize endpoint rejects the request with
invalid_target(no consent screen, an instant bounce) unless a resource indicator is present. Its value is the JMAP session URL. - Redirect: Fastmail's dynamic registration accepts only a reverse-DNS private-use scheme (it refuses http and loopback), so the wizard pins
endpoints.redirection = "org.pimalaya.ortie://redirect". A desktop browser cannot route that scheme back to ortie, soauth getprints a manualauth resumecommand to finish the flow by hand.
endpoints.authorization = "https://api.fastmail.com/oauth/authorize"
endpoints.token = "https://api.fastmail.com/oauth/refresh"
scopes = ["urn:ietf:params:oauth:scope:mail", "urn:ietf:params:oauth:scope:contacts", "urn:ietf:params:oauth:scope:calendars", "offline_access"]
extras.resource = "https://api.fastmail.com/jmap/session"The wizard selects all four advertised scopes by default (Fastmail cannot complete on a desktop anyway; trim them in the scope multi-select if you want). The pre-registered Thunderbird application offered by the wizard covers Fastmail with a loopback redirect instead, avoiding the manual resume; see docs/providers.md and docs/discovery-layering.md for why dynamic registration forces the private-use scheme.
Every command and subcommand is documented through --help. The common flows:
ortie # discover an account and print a config fragment
ortie auth get # request a first access token through the browser
ortie auth resume <url> # finish the flow by hand when the redirection fails
ortie token show # print the stored access token
ortie token refresh # force a refresh
ortie token inspect # show token metadata (type, scopes, expiry)Logs go to stderr; --log-level and --log-file control verbosity and destination, and --json switches output to machine-readable objects.
- pizauth: daemon-oriented alternative
- oama: Haskell alternative
- mutt_oauth2.py: Python script alternative
This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.
- Tools: Claude Code (Anthropic), invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.
- Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.
- Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.
- Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit. Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour.
- Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.
- Last reviewed: 16/07/2026
This project is licensed under either of:
at your option.
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
Contributions are welcome: start with CONTRIBUTING.md, which opens with the Pimalaya-wide guides to read first.
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2027 in preparation…
If you appreciate the project, feel free to donate using one of the following providers:
