Connectors: bring your own OAuth app

Operator·5 minutes to read

F-HOME-2: a home deployment brings its own OAuth application for every connector it uses, rather than sharing one Botseon operates. This page is the setup half: the OAuth framework that uses what you enter here — the authorisation flow, the token exchange and the refresh — ships in @botseon/connectors, and a module names the provider and scopes it needs in its manifest's oauth field (see docs/reference/module-manifest.md). Per-connector pages (starting with gmail.md) are the console steps for one provider at a time.

Why bring your own

Two reasons, and either alone would be enough.

Nobody's client secret is shared. A managed (SaaS) deployment can run one OAuth app on behalf of every tenant, because Botseon operates the servers those tokens are used from. A self-hosted deployment has no equivalent trust boundary: a shared client secret baked into this repository would be a secret every home operator who ever cloned it could read, which is not a secret at all.

A self-hosted deployment is not a verified publisher. Google (and most providers) show an "unverified app" warning — or refuse restricted scopes outright without a completed app-review process — for an OAuth app that has not gone through their own verification, which is a per-application process a shared app could complete once and a thousand home installs could not inherit. Your own OAuth app, created under your own Google/Microsoft/etc. account, sidesteps that entirely for scopes at the level a single operator actually needs: it warns whoever consents (you) that the app is unverified, and that is the whole cost.

Finding your redirect URL

Every provider's OAuth console asks for a redirect URL to send the browser back to after consent. It has the same shape for every connector:

<BOTSEON_PUBLIC_URL>/api/oauth/<plugin>/callback

<plugin> is the plugin's stable, lower-case slug — gmail, for the first one. The plugin's, not a connector's: one install is one OAuth app, one consent and one redirect URL, whatever number of connectors it bundles. microsoft-365 is where the two differ — it is one plugin over outlook-mail, outlook-calendar and onedrive, so its redirect URL says microsoft-365 and there is no URL for any of the three by name. With the quickstart's default, unconfigured BOTSEON_PUBLIC_URL (http://127.0.0.1:3000), Gmail's redirect URL is:

http://127.0.0.1:3000/api/oauth/gmail/callback

Set BOTSEON_PUBLIC_URL first if you are serving the LAN — the redirect URL has to be the address your browser can actually reach, the same way the sign-in link does. @botseon/connectors's redirectUrlFor(publicUrl, connectorSlug) is what builds this string in code (packages/connectors/src/oauth.ts) — the same function the authorisation request itself uses, so the two cannot disagree about a single character, which is the single most common OAuth setup failure and the one a provider's error message never explains. You never have to run it yourself: botseon oauth-app set and botseon oauth-app show both print it (below).

Where the client secret goes, and where it never goes

The client id and client secret your provider's console gives you go in through one command, per organisation and per connector:

printf %s "$CLIENT_SECRET" | botseon oauth-app set \
  --org <organisation id> --slug gmail \
  --client-id <the client id from the console> \
  --auth-url https://accounts.google.com/o/oauth2/v2/auth \
  --token-url https://oauth2.googleapis.com/token \
  --configured-by <your user id>

The secret is read from standard input and there is no flag that takes it. A flag's value is in ps output, in your shell's history file and in the ledger of whatever ran the command; an environment variable is in /proc/<pid>/environ and in every child process. Both URLs must be https:, and the command refuses them otherwise rather than letting a member find out at their consent screen. It stores the secret encrypted in oauth_apps — the same envelope encryption every other secret in this deployment is under — and prints the redirect URL to paste into the provider console, and nothing else.

botseon oauth-app show --org <id> --slug <plugin> says whether an app is configured, under which client id, and what the redirect URL is. Both subcommands refuse a --slug that is not a known plugin, so a typo is caught at the command rather than at a member's consent screen. There is no subcommand and no flag that prints the secret back: once it is in, the only thing that ever reads it is the token exchange.

The secret never goes anywhere a model reads as ordinary text: not into a bot's description, not into a memory, not into a routine's configuration. A model that can read a secret can also repeat it, in a reply, in a tool call's arguments, in whatever gets logged along the way — the same reasoning that keeps BOTSEON_MASTER_KEY and every other generated credential out of application code entirely, and out of anything a bot's own context window would ever hold.

The plugin sheet in the web app shows the same three things the show subcommand does — the redirect URL with a copy control, and whether an app is configured for this connector — and no more.

Writing a connector

The two pages above are the operator's half — setting up an OAuth app for a connector that already exists. The developer's half is next door:

  • adding-a-connector.md — the walk-through, from deciding the tools to clicking the finished plugin through.
  • conformance.md — the thirteen checks every connector has to pass, one section per identifier the gate prints.
  • examples/uptime-connector — the smallest complete one, with a row in its README for each section of the walk-through.

Last verified against build c0f77aa.