Files
PaperClipAI/docs/oauth-integrations.md
T
DottaandPaperclip 382cb3bede feat(oauth): restack OAuth backbone
Restack the OAuth backend from PR #5584 onto current master for PAP-9233, preserving the provider registry, OAuth routes, refresh worker, resolver integration, tests, and docs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-12 09:47:09 -05:00

107 lines
5.2 KiB
Markdown

# OAuth Integrations — Operator Guide
## Overview
Paperclip ships with a built-in OAuth backbone supporting GitHub, Notion,
Slack, Linear, Atlassian, Google Workspace, and Microsoft Graph. Each
provider is opt-in: a provider is registered iff its `client_id` and
`client_secret` env vars are both set at startup. Providers with missing
credentials are skipped with a `WARN` log and never appear at
**Settings → Connections**.
## Setup per provider
For each provider you want to enable:
1. Register an OAuth app in the provider's developer console.
2. Configure the redirect URI as: `${PAPERCLIP_PUBLIC_URL}/api/oauth/callback/<provider-id>`
3. Set both env vars before starting the server:
| Provider | client_id env | client_secret env |
|---|---|---|
| github | `GITHUB_OAUTH_CLIENT_ID` | `GITHUB_OAUTH_CLIENT_SECRET` |
| notion | `NOTION_OAUTH_CLIENT_ID` | `NOTION_OAUTH_CLIENT_SECRET` |
| slack | `SLACK_OAUTH_CLIENT_ID` | `SLACK_OAUTH_CLIENT_SECRET` |
| linear | `LINEAR_OAUTH_CLIENT_ID` | `LINEAR_OAUTH_CLIENT_SECRET` |
| atlassian | `ATLASSIAN_OAUTH_CLIENT_ID` | `ATLASSIAN_OAUTH_CLIENT_SECRET` |
| google-workspace | `GOOGLE_OAUTH_CLIENT_ID` | `GOOGLE_OAUTH_CLIENT_SECRET` |
| microsoft-graph | `MICROSOFT_OAUTH_CLIENT_ID` | `MICROSOFT_OAUTH_CLIENT_SECRET` |
4. Restart the server.
5. Tiles for that provider appear at **Settings → Connections**. Admins can connect.
## Adding a custom provider
The bundled YAML configs live at `server/oauth-providers/`. To add a custom
provider, drop a YAML file in that directory (or set
`PAPERCLIP_OAUTH_PROVIDERS_DIR` to a directory of your own and put the YAML
there — entries from that directory are loaded after and merged with the
bundled configs). Set the corresponding `client_id` and `client_secret` env
vars and restart the server. See `server/oauth-providers/github.yaml` for the
canonical example.
For providers with non-standard response shapes (Slack's `team` block,
Microsoft's `id_token` claim parsing, etc.), add a TypeScript shape module
under `server/src/oauth/shapes/` and wire it from the YAML's `shape:` field.
See `server/src/oauth/shapes/slack.ts` for the canonical example.
## Plugin-contributed providers
Plugins can ship OAuth providers via the `oauthProviders` block in the
plugin manifest. See `packages/plugins/sdk/src/define-oauth-provider.ts`
for the helper, and the plugin developer docs for the manifest schema.
A YAML entry with the same provider id always wins; plugin contributions
for a duplicate id are skipped with a `WARN` log.
## Operations
- **Refresh worker:** runs every 60s, leader-elected via Postgres
advisory lock. No additional config needed.
- **Token storage:** OAuth tokens live in `company_secrets` /
`company_secret_versions`, encrypted via the configured `SecretProvider`.
- **Logs:** all OAuth code paths emit structured pino logs with
`component=oauth`. Token material is never logged — `access_token`,
`refresh_token`, `id_token`, `code`, `code_verifier`, and `client_secret`
are redacted at the logger level.
- **Metrics:** `oauth_refresh_total`, `oauth_refresh_duration_seconds`,
`oauth_connections_by_status` (Prometheus, existing pattern).
## Troubleshooting
- **"Provider isn't configured":** check both env vars are set; restart
to pick up changes.
- **"Refresh stalled":** check `last_error` on the connection row; usually
provider-side (network blip). Manual `Refresh now` from the UI clears
the backoff.
- **"Account mismatch":** user authorized a different account than the
existing connection. Disconnect first, then reconnect.
## Known limitations
- **Linear `accountInfo`** uses `https://api.linear.app/oauth/userinfo`
(Linear's OIDC userinfo endpoint). If Linear changes that endpoint, the
YAML at `server/oauth-providers/linear.yaml` will need to update or grow
a custom shape module.
- **Per-tenant rate limit** on `POST /oauth/connect/:providerId` is 50
requests per 5 minutes (state-row flood guard). This is in addition to
the existing per-user limit.
- **Refresh worker leader-election** uses
`pg_try_advisory_xact_lock` inside a single transaction so the lock is
released automatically at COMMIT/ROLLBACK regardless of which pool
connection the underlying postgres-js client picked up. Multi-process
deployments coordinate naturally through the shared lock.
- **`mark-revoked` middleware** is wired in `app.ts` and reads a Bearer
run-JWT via `verifyLocalAgentJwt`. The endpoint returns 401 without a
valid JWT carrying an `oauth.connectionIds` claim, 403 when the claim
doesn't include the requested connection, 204 on success.
- **GitHub upstream revocation is best-effort.** GitHub's
`/applications/{client_id}/grant` endpoint requires `DELETE` and a
JSON body (`{"access_token": "..."}`), not RFC-7009's
`POST` + form-encoded shape that `revokeUpstreamToken` uses. The
upstream revoke call therefore always fails for GitHub — the local
disconnect cleanup path swallows the error and proceeds, so the
user's local state ends up correct, but the GitHub grant remains
valid until natural expiry or until the user revokes it via the
GitHub UI. A follow-up PR will add a per-provider `revokeStyle`
field to handle non-RFC-7009 providers.