pimalaya/ortie
pimalaya/ortie has 83 GitHub stars and 7 forks. Star changes are not measured yet. Last checked 2026-11-09 09:09 UTC. Last push 2026-11-08.
CLI to manage OAuth tokens
What you can do
- Configuration wizard: discovers your provider's grants and writes the account for you.
- Dynamic client registration: registers a public client on the spot, no provider console.
- Authorization code grant: browser sign-in, with a built-in server catching the redirection.
- Device authorization grant: a short code typed on another device, for hosts with no browser.
Measured star growth
- in 24h
- Collecting history
- in 7 days
- Collecting history
- in 15 days
- Collecting history
- in 1 month
- Collecting history
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
README.md
Original🔑 Ortie

CLI to manage OAuth 2.0 tokens, written in Rust
Table of contents
- Features
- Coverage
- Installation
- Configuration
- Usage
- Alternatives
- AI policy
- License
- Social
- Contributing
- Sponsoring
Features
- Configuration wizard: discovers your provider's grants and writes the account for you.
- Dynamic client registration: registers a public client on the spot, no provider console.
- Authorization code grant: browser sign-in, with a built-in server catching the redirection.
- Device authorization grant: a short code typed on another device, for hosts with no browser.
- Client credentials grants: headless machine tokens, by client secret or signed JWT assertion.
- Manual completion: finish a handed-off flow with
auth listen, or by hand withauth resume. - Token refresh: on demand, or automatically when the token is read.
- Token revocation: retire a token at the authorization server.
- PKCE: S256 by default, following the OAuth 2.1 posture.
- Extra parameters: provider-specific authorization parameters forwarded verbatim.
- Token storage: read and write tokens through your own shell commands.
- Hooks: a shell command or a desktop notification on issuance and refresh.
- Persistent session: unlock the secret store once, then answer token commands over stdin.
- MCP server: an agent drives the grants through
ortie mcp, and no tool returns a token. - JSON output:
--jsonon every data command, its payload described byortie json-schema. - TLS: rustls with ring (
rustls-ring, default) or aws (rustls-aws) crypto, or native-tls (native-tls).
[!TIP] Ortie is written in Rust and uses cargo features to gate optional functionality. The default feature set is declared in Cargo.toml.
The default
wizardfeature brings the interactiveconfigure; leave it out of a scripted or embedded build that never prompts.
Coverage
| RFC | What is covered |
|---|---|
| 6749 | The OAuth 2.0 framework: authorization code and client credentials grants, access token issuance and refresh |
| 7009 | Token revocation: token revoke retires the refresh token, or the access token when none is stored |
| 7523 | JWT client authentication on the client credentials grant: assertion signed with a private key, certificate x5t thumbprint (Microsoft certificate credentials) |
| 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 |
| 8628 | Device authorization grant: device and user code request, token endpoint polling |
| 8707 | Resource indicators: the resource parameter a server bound to one resource requires |
| 9728 | Protected resource metadata: the wizard reads a service's resource and the authorization servers it names |
OAuth 2.1
Ortie follows the OAuth 2.1 draft and the security best current practice (RFC 9700):
- Grants: authorization code, device, and client credentials authenticated by secret or signed JWT. The implicit and resource owner password grants, removed by OAuth 2.1, are not implemented.
- PKCE: S256 on every authorization code flow by default,
pkce = "plain"orfalsebeing escape hatches for servers that need them. - Redirects: a configured redirection URI is sent as written, for the server to match exactly. Without one, Ortie binds
http://127.0.0.1on a random port, the loopback exception that lets the port vary (RFC 8252 section 7.3). - Refresh rotation: a refreshed token is stored as soon as the server answers, keeping the previous refresh token when no rotated one comes back. When storing fails, the error says so, and a REPL session keeps the new tokens.
Installation
Pre-built binary
Ortie can be installed with the installer:
As root:
curl -sSL https://raw.githubusercontent.com/pimalaya/ortie/master/install.sh | sudo sh
As a regular user:
curl -sSL https://raw.githubusercontent.com/pimalaya/ortie/master/install.sh | PREFIX=~/.local sh
These 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
cargo install --locked ortie
For the git tip:
cargo install --locked --git https://github.com/pimalaya/ortie.git
Nix
If you have the Flakes feature enabled:
nix profile install github:pimalaya/ortie
Or run without installing:
nix run github:pimalaya/ortie
Sources
git clone https://github.com/pimalaya/ortie
cd ortie
nix run
Configuration
Run ortie with no command and it offers to generate a first account, which ortie configure does again later. From an email address, a domain or an issuer URL it discovers the grants your provider offers, then appends the account it walked you through to your configuration.
What it cannot discover it does not ask for: the annotated config.sample.toml is the reference for the rest. Run ortie auth get afterwards to authorize the account and store its first token.
Another tool can hand its OAuth 2.0 setup over: ortie configure [email protected] --name work --service imap --service smtp skips the address prompt, names the account work as written, and offers only the grants serving IMAP or SMTP. When work already exists, it asks whether to keep that account instead.
A configuration is loaded from the first valid path among $XDG_CONFIG_HOME/ortie/config.toml, $HOME/.config/ortie/config.toml and $HOME/.ortierc. Override it with -c <PATH> or ORTIE_CONFIG=<PATH>, :-separated to deep-merge several files on top of the first.
An OAuth 2.0 application is needed too. The wizard offers three, most preferred first: dynamic registration when the provider advertises it, a public application (Thunderbird credentials cover most consumer providers), or your own, left as an empty client-id to fill in.
Ready-made blocks for common providers follow, for manual setups and for Microsoft Graph, which the wizard does not cover. Drop one 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 endpoints, not the legacy o/oauth2/auth and oauth2/v3/token pair, which Google can reject at consent with "This app is blocked".
Gmail and CardDAV being sensitive scopes, an unverified application of your own only works for accounts listed as test users. The Thunderbird one below is verified.
Contacts are split across two scopes that are not interchangeable: auth/carddav authorizes the CardDAV endpoint, auth/contacts the People API. Calendars are not split, CalDAV using the plain auth/calendar scope.
A client id is verified for a fixed set of scopes, so asking the Thunderbird application for a People API scope fails at consent.
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.
Microsoft (Outlook IMAP / SMTP)
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"
Microsoft Graph
The Thunderbird application above is registered for Outlook IMAP and SMTP, not for the Graph API. Graph tokens need 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 accounts may get an opaque token it rejects with InvalidAuthenticationToken, so prefer a work or school account, or an application of your own.
Fastmail
Fastmail advertises RFC 7591 dynamic registration, so bare ortie registers a client for you. Two specifics it fills in, worth knowing when writing the block by hand:
- RFC 8707 resource: without a resource indicator, the authorize endpoint bounces the request with
invalid_target, before any consent screen. Its value is the JMAP session URL, which the wizard reads from the session's RFC 9728 metadata. - Redirect: dynamic registration accepts only a reverse-DNS private-use scheme,
org.pimalaya.ortie://redirect. No desktop browser routes it back, soauth getprints a manualauth resumecommand.
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"]
resources = ["https://api.fastmail.com/jmap/session"]
The wizard selects the four scopes the JMAP session advertises by default, and the multi-select is where you trim them. The Thunderbird application it also offers covers Fastmail with a loopback redirect, avoiding the manual resume.
Usage
Configure an account, authorize it, then read its token:
ortie configure # discover a provider and write the account
ortie auth get # authorize and store a first token
ortie auth listen --state=… --pkce=… --redirect-uri=… # catch a handed-off redirection
ortie auth resume <URI|DEVICE_CODE> # finish a flow by hand
ortie token show # print the stored access token
ortie token refresh # force a refresh
ortie token inspect # print type, scopes and expiry
ortie token revoke # revoke the token at the server
ortie repl answers those token commands from stdin instead, reading the secret store once so a keyring is unlocked one time rather than per call:
printf 'token show\n' | ortie repl
ortie mcp runs a Model Context Protocol server over stdio for one account, so an agent can obtain and renew its token. auth_get answers the authorization URI to open, or the user code to enter, and the server keeps the state, the PKCE verifier and the device code to itself: it catches the redirection or polls the device code, then stores the token once the user consents, and auth_status reports the outcome. auth_resume takes the redirected URL when no listener can catch it, and token_inspect and token_refresh work on the stored token. No tool returns the token, which applications keep reading through ortie token show; token_revoke is listed only with --allow-revoke.
{
"mcpServers": {
"ortie": { "command": "ortie", "args": ["mcp", "--account", "work"] }
}
}
Every command and every flag is documented behind --help. --json switches data commands to machine-readable objects, ortie json-schema --dir <DIR> writing the JSON Schema of each, and logs go to stderr, with --log-level <LEVEL> and --log-file <PATH> setting their verbosity and destination.
Alternatives
- pizauth: daemon-oriented alternative
- oama: Haskell alternative
- mutt_oauth2.py: Python script alternative
License
This project is licensed under either of:
Social
- Chat on Matrix
- News on Mastodon or RSS
- Mail at [email protected]
Sponsoring
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
- 2026 → 2027: NGI Zero Commons Fund
This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
561,250 starspublic-apis/public-apis
A collective list of free APIs
497,507 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
445,844 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,231 stars