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>
5.2 KiB
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:
-
Register an OAuth app in the provider's developer console.
-
Configure the redirect URI as:
${PAPERCLIP_PUBLIC_URL}/api/oauth/callback/<provider-id> -
Set both env vars before starting the server:
Provider client_id env client_secret env github GITHUB_OAUTH_CLIENT_IDGITHUB_OAUTH_CLIENT_SECRETnotion NOTION_OAUTH_CLIENT_IDNOTION_OAUTH_CLIENT_SECRETslack SLACK_OAUTH_CLIENT_IDSLACK_OAUTH_CLIENT_SECRETlinear LINEAR_OAUTH_CLIENT_IDLINEAR_OAUTH_CLIENT_SECRETatlassian ATLASSIAN_OAUTH_CLIENT_IDATLASSIAN_OAUTH_CLIENT_SECRETgoogle-workspace GOOGLE_OAUTH_CLIENT_IDGOOGLE_OAUTH_CLIENT_SECRETmicrosoft-graph MICROSOFT_OAUTH_CLIENT_IDMICROSOFT_OAUTH_CLIENT_SECRET -
Restart the server.
-
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 configuredSecretProvider. - 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, andclient_secretare 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_erroron the connection row; usually provider-side (network blip). ManualRefresh nowfrom the UI clears the backoff. - "Account mismatch": user authorized a different account than the existing connection. Disconnect first, then reconnect.
Known limitations
- Linear
accountInfouseshttps://api.linear.app/oauth/userinfo(Linear's OIDC userinfo endpoint). If Linear changes that endpoint, the YAML atserver/oauth-providers/linear.yamlwill need to update or grow a custom shape module. - Per-tenant rate limit on
POST /oauth/connect/:providerIdis 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_lockinside 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-revokedmiddleware is wired inapp.tsand reads a Bearer run-JWT viaverifyLocalAgentJwt. The endpoint returns 401 without a valid JWT carrying anoauth.connectionIdsclaim, 403 when the claim doesn't include the requested connection, 204 on success.- GitHub upstream revocation is best-effort. GitHub's
/applications/{client_id}/grantendpoint requiresDELETEand a JSON body ({"access_token": "..."}), not RFC-7009'sPOST+ form-encoded shape thatrevokeUpstreamTokenuses. 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-providerrevokeStylefield to handle non-RFC-7009 providers.