๐Ÿ”‘ Ortie [](https://matrix.to/#/#pimalaya:matrix.org) [](https://fosstodon.org/@pimalaya) [](https://pimalaya.org/sponsor/)

August 15, 2026 ยท View on GitHub

CLI to manage OAuth 2.0 tokens, written in Rust

Table of contents

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 flow by hand when the redirection server cannot bind.
  • Token refresh: on demand, or automatically when the token is read.
  • 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.
  • JSON output: --json on every data command, for scripts.
  • 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.

Coverage

RFCWhat is covered
6749The OAuth 2.0 framework: authorization code and client credentials grants, access token issuance and refresh
7523JWT client authentication on the client credentials grant: assertion signed with a private key, certificate x5t thumbprint (Microsoft certificate credentials)
7636PKCE: the S256 and plain code challenges protecting the authorization code in transit
7591Dynamic client registration: register a public client without any provider console
8414Authorization server metadata: the wizard reads it to discover a provider's endpoints and registration endpoint
8628Device authorization grant: device and user code request, token endpoint polling

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: 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, walks you through the application, the scopes and the token storage, then appends the account 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.

A configuration is loaded from the first valid path among XDGCONFIGHOME/ortie/config.toml,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.

Google

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 can register a client for you. Two specifics it fills in, worth knowing when writing the block by hand:

  1. 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.
  2. Redirect: dynamic registration accepts only a reverse-DNS private-use scheme, org.pimalaya.ortie://redirect. No desktop browser routes it back, so auth get prints a manual auth resume command.
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; trim them in the multi-select. 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 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 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

Every command and every flag is documented behind --help. --json switches data commands to machine-readable objects, and logs go to stderr, --log-level <LEVEL> and --log-file <PATH> setting their verbosity and destination.

Alternatives

License

This project is licensed under either of:

Social

Sponsoring

nlnet

Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:

This program is part of Pimalaya, free software funded entirely by grants and donations. If you find it useful, consider sponsoring its development:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal