Skip to content
Projects

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.

How these counts are measured

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 Matrix Mastodon Sponsor

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 handed-off flow with auth listen, or by hand with auth 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: --json on every data command, its payload described by ortie 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 wizard feature brings the interactive configure; 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" or false being 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.1 on 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.

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 registers 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, which the wizard reads from the session's RFC 9728 metadata.
  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"]
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

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

Explore related projects