Sign-in Providers

Sign-in Providers

Some services have no key to paste. You do not get a QuickBooks API key the way you get a Stripe one — you sign in, consent to a scope, and the service hands back a token. TAP runs that flow for you: the tokens are minted server-side, stored encrypted, pinned to the service’s own API hosts, and refreshed or extended before a call that needs it. No token ever reaches the agent.

This page covers the providers configured by table. Google and Microsoft are sign-in providers too, with their own implementation and the same agent-facing shape — see Credential Setup.

What a provider is

A provider is a row of configuration, not code:

FieldWhat it is
idURL segment and stored-bundle discriminator (quickbooks)
labelWhat a person reads on a connect card
authorize_url / token_urlThe provider’s two OAuth endpoints
scopesSelectable permission bundles, (id, label, scopes) — callers send bundle ids, never raw scope strings
pkceSend an RFC 7636 S256 challenge on authorize and the verifier on exchange
client_authbasic (HTTP Basic) or post (form fields) at the token endpoint
extra_authorize_paramsQuery parameters every consent URL for this provider carries
callback_params_to_keepCallback parameters that are part of the connection, not the handshake — QuickBooks’ realmId
allowed_hostsHost pin for the created credential; never empty
token_headerHeader the access token is injected into (default Authorization: Bearer)
refresh_rotatesThe provider issues a new refresh token on every redemption and kills the old one
send_scope / scope_separatorWhether the consent URL carries scope, and how scopes are joined (Meta wants commas)
authorize_param_varsConsent URL parameters read from the deployment — Facebook’s config_id
lifecycleRefreshToken (RFC 6749), or LongLived for providers that issue no refresh token (Meta): an optional exchange for a long-lived token at consent, and an optional in-place extension before expiry
resource_token_lookupPer-resource tokens fetched at consent and injected when a request’s path names that resource — Facebook Page tokens
redact_response_keysJSON keys redacted from every response to the credential — Meta’s access_token
token_prefix_overridesHosts that take a different token prefix — Meta’s upload host takes OAuth

The table lives in crates/tap-proxy/src/oauth_providers.rs. Adding a provider is a row plus two environment variables on the deployment — no new handler, no new callback, no new routing branch.

Endpoints

Each configured provider gets the same four routes, from one set of handlers:

RouteWho calls it
POST /team/oauth/{provider}/startThe dashboard — session-authenticated, owners and admins
POST /team/oauth/{provider}/reauthorizeThe dashboard, to repair an existing credential
POST /app/users/{ext_id}/credentials/oauth/{provider}/startA partner’s app key, for a managed end-user
POST /agent/credentials/oauth/{provider}/startA host app’s connect card, with the signed-in person’s token
GET /oauth/{provider}/callbackThe provider, returning the browser

GET /agent/connectors lists every provider this deployment offers with kind: "oauth", its scope bundles, and available — false when the client id or secret is missing here, with unavailable_reason naming the variable (never a value). A connect card reads that listing and needs no per-provider code of its own.

The agent start endpoint refuses an X-TAP-Key outright: consent is a human act, so it takes the signed-in person’s own token and applies their workspace role. Full request and response shapes: API Reference.

GET /team/oauth/providers is the same listing for the dashboard, which holds a session rather than an agent key. It returns every row as {id, label, available, unavailable_reason?, scopes, default_scopes, api_base, allowed_hosts}; unavailable_reason names the missing variable, never a value. Any workspace member may read it. Starting or reauthorizing a flow is still owners and admins only.

In the dashboard

Add credential lists every provider on the table under Sign-in providers, built from GET /team/oauth/providers, so a new row shows up with no dashboard change. A provider this deployment has not configured appears as a disabled tile naming the missing variable. Picking one asks for a credential name, a description and the scope bundles (defaults pre-checked), then sends the person to the provider’s consent screen.

Credentials a provider created are recognised by their exact api_base and badged with the provider’s label. Each such row has a Reconnect button, which calls POST /team/oauth/{provider}/reauthorize and keeps the scopes the credential already has. When consent returns, the callback’s dashboard redirect carries provider=<id> next to oauth= (and reason= on failure), so the result banner names the provider.

Using the credential

The credential lands as an ordinary TAP credential: reference it by name in X-TAP-Credential and call /forward as usual. TAP injects the provider’s token header — the agent never sees a token and cannot ask for one.

Two things worth knowing:

  • The host pin is real. The credential is bound to the provider’s own API hosts at creation. A /forward aimed anywhere else is refused before any injection, so a prompt-injected agent has nowhere to send the token.
  • Connection parameters are not secrets. Anything the provider’s callback returns that the API needs in the URL — QuickBooks’ company id — is stored with the credential and surfaced in that credential’s /agent/services entry under its own field (realm_id), alongside oauth_provider. The agent puts it in the URL itself.

Access tokens are cached in the stored bundle and refreshed only when expired. On a provider that rotates its refresh token, the new one is written back atomically, so two proxy instances refreshing at once cannot leave the credential holding a token the provider has already invalidated.

When a refresh finally fails with invalid_grant, /forward answers 502 with error_code: "oauth_reauth_required" and a reauth_url — the person reconnects and the same credential name keeps working.

QuickBooks Online

Provider idquickbooks
Authorizehttps://appcenter.intuit.com/connect/oauth2
Tokenhttps://oauth.platform.intuit.com/oauth2/v1/tokens/bearer
Client authHTTP Basic only. Intuit rejects the secret in the form body, and answers invalid_client when client_id is repeated in the body alongside the header
Scope bundlesaccounting (com.intuit.quickbooks.accounting, the default), payments (com.intuit.quickbooks.payment)
Kept callback parameterrealmIdrealm_id
Allowed hostsquickbooks.api.intuit.com, sandbox-quickbooks.api.intuit.com
Refresh tokenRotates on every redemption; expires after 100 days idle
Access tokenOne hour

Deployment setup

Create an app in the Intuit developer portal, and register TAP’s callback as a redirect URI exactly as the proxy will send it:

https://<your TAP proxy host>/oauth/quickbooks/callback

Then set:

QUICKBOOKS_OAUTH_CLIENT_ID=...
QUICKBOOKS_OAUTH_CLIENT_SECRET=...
# Optional — only if the callback URL differs from {TAP_PROXY_URL}/oauth/quickbooks/callback
QUICKBOOKS_OAUTH_REDIRECT_URI=https://proxy.example/oauth/quickbooks/callback

Until both the id and the secret are set, /agent/connectors reports QuickBooks as available: false and every start endpoint answers 503 — a connect card renders no dead button.

Intuit issues sandbox keys immediately and production keys after review; the sandbox host (sandbox-quickbooks.api.intuit.com) is in the host pin so the same credential works against either.

Calling the API

QuickBooks addresses a company, so every request path carries the realm id. Get it from the credential’s /agent/services entry — never invent one:

# realm_id comes from GET /agent/services -> services["quickbooks"].realm_id
curl -X POST "$TAP_PROXY_URL/forward" \
  -H "X-TAP-Key: $TAP_AGENT_KEY" \
  -H "X-TAP-Credential: quickbooks" \
  -H "X-TAP-Target: https://quickbooks.api.intuit.com/v3/company/$REALM_ID/companyinfo/$REALM_ID" \
  -H "X-TAP-Method: GET" \
  -H "Accept: application/json"

Facebook

Provider idfacebook
LoginFacebook Login for Business — https://www.facebook.com/v25.0/dialog/oauth
Tokenhttps://graph.facebook.com/v25.0/oauth/access_token
Client authForm fields
ScopesNone on the URL: the login configuration (config_id) sets the permissions
Allowed hostsgraph.facebook.com, rupload.facebook.com
Access tokenA system-user token that does not expire (a user-token configuration is exchanged for a 60-day token)
Page tokensFetched at consent from /me/accounts, and injected when a request’s path names that Page

Deployment setup

In Meta for Developers, create a Business app and add Facebook Login for Business. Create a login configuration with token type System-user access token, the Pages asset (and Instagram accounts, to reach them through Facebook), and the permissions you need — typically pages_show_list, pages_read_engagement and pages_manage_posts, plus instagram_basic and instagram_content_publish for linked Instagram accounts. Copy the configuration id.

Register TAP’s callback as a valid OAuth redirect URI:

https://<your TAP proxy host>/oauth/facebook/callback

Then set:

FACEBOOK_OAUTH_CLIENT_ID=...      # the Meta app id
FACEBOOK_OAUTH_CLIENT_SECRET=...  # the Meta app secret
FACEBOOK_OAUTH_CONFIG_ID=...      # the login configuration id

Until all three are set, /agent/connectors reports Facebook available: false, naming the first missing variable.

Only people with a role on the Meta app can sign in until Meta grants Advanced Access for each permission through App Review, which requires Business Verification. That review is what lets another company’s people connect their Pages.

Calling the API

The credential’s /agent/services entry lists the connected Pages as page_ids, and their linked Instagram accounts as instagram_business_account_ids. Name a Page in the path and TAP sends that Page’s token:

curl -X POST "$TAP_PROXY_URL/forward" \
  -H "X-TAP-Key: $TAP_AGENT_KEY" \
  -H "X-TAP-Credential: facebook" \
  -H "X-TAP-Target: https://graph.facebook.com/v25.0/$PAGE_ID/feed" \
  -H "X-TAP-Method: POST" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello from our agent"}'

Any other path — /me, an Instagram account id — gets the system-user token. Pages are captured at consent, so after granting another Page, reconnect.

Every access_token field in a response is redacted, so /me/accounts or ?fields=access_token never hands the agent a token. Resumable Page video uploads to rupload.facebook.com name no Page in their path and are not supported yet; publish video by file_url instead.

Instagram

Provider idinstagram
LoginInstagram API with Instagram Login — https://www.instagram.com/oauth/authorize
Tokenhttps://api.instagram.com/oauth/access_token, then https://graph.instagram.com/access_token for the long-lived token
Client authForm fields
Scope bundlesbasic (instagram_business_basic), publish (+ instagram_business_content_publish), comments (+ instagram_business_manage_comments), messages (+ instagram_business_manage_messages); default basic and publish
Allowed hostsgraph.instagram.com, rupload.facebook.com
Access token60 days. TAP extends it in place once fewer than 30 days remain, at most once a day

For Instagram professional (Business or Creator) accounts; no Facebook Page is needed. Every bundle includes instagram_business_basic, because Meta refuses to extend a token without it.

Deployment setup

In your Meta app, add Instagram → API setup with Instagram login, and register this redirect URI under its Business login settings:

https://<your TAP proxy host>/oauth/instagram/callback

Copy the Instagram app id and secret shown there — they are not the Meta app’s own id and secret — and set:

INSTAGRAM_OAUTH_CLIENT_ID=...
INSTAGRAM_OAUTH_CLIENT_SECRET=...

As with Facebook, accounts outside the app’s own roles need Advanced Access through App Review.

Calling the API

curl -X POST "$TAP_PROXY_URL/forward" \
  -H "X-TAP-Key: $TAP_AGENT_KEY" \
  -H "X-TAP-Credential: instagram" \
  -H "X-TAP-Target: https://graph.instagram.com/v25.0/me?fields=user_id,username" \
  -H "X-TAP-Method: GET"

If the token lapses anyway — access revoked, a password change — /forward answers 502 with error_code: "oauth_reauth_required" and a reauth_url.

Adding a provider

  1. Add a row to PROVIDERS in crates/tap-proxy/src/oauth_providers.rs. The table tests check every row for the invariants the handlers assume — a non-empty host pin, default bundles that exist in the catalog, an id that is URL-safe and does not shadow google or microsoft.
  2. Name the row’s environment variables — the client id and secret, plus any authorize_param_vars — and set them on the deployment.
  3. Register the callback URL, {TAP_PROXY_URL}/oauth/{id}/callback, with the provider’s developer app.

There is no step 4. Routes, the connect-card listing, the consent flow, the callback, forward-time refresh and rotation write-back all read the row.