auther

auther is a central repository for OAuth tokens. It performs the authorization code flow against any provider you describe to it, keeps the resulting tokens refreshed for as long as it runs, and hands the live access token to downstream projects that present an API key.

A project that needs an OAuth token otherwise carries the whole apparatus itself: a browser flow, somewhere to put a refresh token, a clock to renew it against, and the failure modes of all three. auther does that once. A downstream project makes one authenticated GET and receives a token that is already current.

The refresh token never leaves. What a downstream project receives is an access token, its expiry, the granted scope, and whatever else the provider sent. A project whose API key is stolen loses the access it was granted; it does not lose the ability to mint new tokens indefinitely.

Running it

auther

That is the whole of it. There are no subcommands and no config file: everything auther knows lives in its own database, and the page it serves is where you put it there.

Variable
AUTHER_ADDR Listen address, :8080 by default
AUTHER_DB Where the database is. ~/.local/share/auther/auther.db by default
AUTHER_LOG_LEVEL error, warn, info, debug or dump
AUTHER_SNIFFER_KEY Ships logs to OpenObserve. Console only without it

Locking

auther starts locked. A fresh database asks you to choose a master password; every later start asks for it. The key is derived from that password with argon2id and exists only in memory, so the database file holds a salt and a verifier and nothing else about it: without the password, and without auther running, the credentials cannot be read.

While locked, /v1 answers 503 and the refresh sweep does not run. That is the cost of the property above: an unattended restart serves nothing until somebody opens it. Refresh tokens survive months of disuse, so nothing is lost by the delay.

Unlocking hands the browser a token derived from the key, held in the tab and nowhere else, so closing the tab or restarting auther asks again.

The page

Describe a provider: two endpoint URLs, a client id, and either PKCE or a client secret. Then type a name for the account and press Enter. The browser goes off to the provider and comes back authorized. Providers are added and edited while auther runs; nothing needs restarting.

The account table shows how long each access token has left, when it was last refreshed and what it was granted, and it keeps showing that live. fetch on a row gives the exact request a downstream project makes against that account. API keys are issued and revoked there too; a new key is readable once, at the moment it is issued.

Accounts

An account is a named slot holding one authorization: provider/name, where the name is yours to choose. auther never reads an identity out of a token, which is what frees it from caring whether the access token is a JWT, what its claims are called, or whether the provider has a userinfo endpoint at all. Several accounts of one provider sit side by side under different names.

The API

/healthz, /callback and the page itself are open. Everything else needs Authorization: Bearer <api key>, and answers 503 while auther is locked.

Route
GET /v1/tokens/{provider}/{account} The current access token, refreshed first if it is near expiry
GET /v1/tokens/{provider} That provider's accounts, without their tokens
GET /v1/accounts The accounts this key is granted, across every provider

The two listings answer with the same rows: a provider, an account name, its state, scope and timestamps, and no credential. They are how a downstream project finds the account names it then asks for tokens by.

{
  "provider": "github",
  "account": "main",
  "access_token": "...",
  "token_type": "Bearer",
  "scope": "repo read:org",
  "expires_at": "2026-09-18T13:04:11Z",
  "extra": { "id_token": "..." }
}

extra carries every member of the token response auther does not act on itself, so a provider-specific field a downstream project needs is still there. The refresh token structurally cannot reach it: parsing removes the fields auther acts on, and what remains is the rest.

Keeping the tokens alive

A sweep runs every minute and renews any access token due within five, so a downstream project that asks once a week still gets a working token, and a refresh token a provider would expire through disuse is exercised long before that. A read refreshes too, which makes the sweep the mechanism and the read path a safety net.

Refreshes are serialized per account and the row is re-read behind the lock. Providers that rotate refresh tokens invalidate the old one the instant the new one is issued, so two refreshes racing on one account would leave the loser holding a token the provider has already forgotten.

A refresh that fails leaves the stored credentials alone and records why, which shows against the row. A provider outage costs nothing permanent; a withdrawn consent is visible rather than silent.

Description
No description provided
Readme 265 KiB
Languages
Go 78.1%
Svelte 16.1%
TypeScript 3%
CSS 1.7%
Shell 0.6%
Other 0.5%