Connector auth patterns
Auth in the Connector SDK is a declaration, never an implementation. The manifest states the kind of credential a connector needs; the platform acquires it per tenant, stores it in the tenant's secret store, refreshes and rotates it, and injects it into ctx.http at call time. Your handler never sees a token. This page covers each pattern in working depth: API keys, both halves of OAuth 2.0, AWS IAM with cross-account roles, rotation, and where the material actually lives.
The declaration model
The whole auth surface of the SDK is one type and three helpers:
export type AuthKind = 'oauth2' | 'api_key' | 'basic' | 'aws_iam' | 'mtls' | 'none';
export interface AuthSchema {
kind: AuthKind;
scopes?: string[]; // meaningful for oauth2 only
}
export function oauth2(opts: { scopes?: string[] } = {}): AuthSchema;
export function apiKey(): AuthSchema;
export function none(): AuthSchema;
Kinds without a helper (basic, aws_iam, mtls) are declared as literals: auth: { kind: 'aws_iam' }. The shape is the API. What the declaration buys you is the split that makes connectors reviewable: everything in this table is the platform's job, and none of it appears in your code.
| Concern | Owner |
|---|---|
| Declaring the kind and scopes | Your def |
| Acquiring the credential (key entry, OAuth flow, role handshake) | Platform, at connection time |
| Storing the material | Tenant secret store (AWS Secrets Manager in the managed platform) |
| Refreshing, rotating, revoking | Platform |
| Injecting it into requests | Runtime, via ctx.http |
API key
The common SaaS case: one long-lived key per vendor account. Declare it with the helper; at connection time the person connecting pastes the key once into the workspace UI or the CLI, and it lands directly in the secret store. Your handler makes plain calls on ctx.http and the runtime attaches the key in whatever form the connector's HTTP profile specifies, header or query, without the handler naming it.
import { defineConnector, tool, apiKey } from '@fibric/connector-sdk';
export default defineConnector({
id: 'cn-brightdesk',
version: '1.0.0',
category: 'comms',
auth: apiKey(),
tools: {
'conversation.read': tool({
handler: async (ctx, args) => {
// ctx.http is pre-authenticated; no key appears in connector code
// return await ctx.http.get(`/v1/conversations/${args.conversation_id}`);
return { id: args.conversation_id };
},
}),
},
});
$ fibric connectors add cn-brightdesk --connection brightdesk-live
auth: api_key
paste the Brightdesk API key (input hidden):
stored in tenant secret store as connections/brightdesk-live
probe: ok (open conversations: 214)
Prefer a key minted for Fibric specifically, scoped as narrowly as the vendor allows, over a shared admin key. When the vendor supports multiple keys, rotation becomes a zero-downtime operation; see rotation.
OAuth 2.0
Declare oauth2() with exactly the scopes your tools need. Review compares declared scopes against your tool surface, and a def asking for write scopes with no side-effecting tool fails review.
auth: oauth2({ scopes: ['conversations.read', 'notes.write'] }),
Authorization code flow
Connecting an oauth2 connector runs the standard authorization code grant, driven entirely by the platform:
- The person connecting clicks through to the vendor's consent screen, carrying your declared scopes and a state parameter bound to the connection.
- The vendor redirects back to the platform's callback with the authorization code. Connector code hosts no callback and sees no code.
- The platform exchanges the code for an access token and, where offered, a refresh token, and writes both to the tenant secret store under the connection.
- The probe runs once to confirm the grant actually works before the connection is marked healthy.
Client credentials for the OAuth app itself (client id and secret) are registered once per connector at publish time, not per tenant, and are held platform-side. A tenant connecting your listing never handles them.
Refresh and expiry
The runtime refreshes proactively: when a call is about to go out on ctx.http and the access token is inside its expiry window, the refresh happens first, under a per-connection lock so concurrent tool calls never race two refreshes against a vendor that rotates refresh tokens on use. Handlers observe none of this. Two failure modes surface to operators of the tenant rather than to your code:
| Condition | What happens |
|---|---|
| Refresh succeeds | New tokens replace the old in the secret store. Invisible everywhere else. |
| Refresh fails (grant revoked, scope withdrawn, vendor reset) | The connection is marked reauth_required, its probe goes unhealthy, tool calls against it fail fast with a stable error, and the tenant is prompted to re-run consent. Side effects are not silently dropped: a plan whose action hits a reauth_required connection gets an ok: false receipt with that reason. |
AWS IAM and cross-account roles
For AWS-native targets, declare { kind: 'aws_iam' }. No long-lived secret exists under this kind at all: the connection stores a role ARN and an external id, and the runtime obtains short-lived credentials by assuming the role at call time. This is the pattern the live cn-amazon-connect connector uses.
Connecting is a cross-account handshake:
- The platform shows the tenant the exact trust policy to create: a role in the tenant's AWS account trusting Fibric's connector principal, conditioned on a connection-specific external id.
- The tenant creates the role, attaches a permissions policy scoped to what the connector's tools need, and pastes the role ARN back.
- The platform performs a test
sts:AssumeRolewith the external id and runs the probe. Only then is the connection healthy.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"connect:GetContactAttributes",
"connect:ListContactFlows"
],
"Resource": "arn:aws:connect:us-east-1:111111111111:instance/EXAMPLE/*"
}]
}
The external id is what prevents the confused-deputy problem: without it, any Fibric tenant that learned your role ARN could ask the platform to assume it. The platform will not complete an aws_iam connection whose role trust policy omits the external id condition.
Token and key rotation
Rotation is a platform operation on the connection, never connector code. What rotation means differs by kind:
| Kind | Rotation mechanics |
|---|---|
oauth2 | Continuous and automatic via refresh. A full re-consent is only needed when the grant itself is revoked. |
api_key | Operator-driven: mint a new key at the vendor, run fibric connectors rotate (or use the connection settings screen) to stage it, the probe verifies the new key, then the swap commits and the old key can be revoked at the vendor. In-flight calls finish on the credential they started with; there is no window where calls fail because both keys are valid across the swap. |
aws_iam | Nothing to rotate: credentials are minted per call by sts:AssumeRole and expire on their own. Rotating means editing the role's permissions policy in the tenant account, which takes effect on the next assumption. |
basic / mtls | Same staged swap as api_key, with the certificate and key pair as the staged material for mtls. |
$ fibric connectors rotate cn-brightdesk --connection brightdesk-live
paste the replacement credential (input hidden):
probe with staged credential: ok
committed. previous credential released; revoke it at the vendor now.
Every rotation is itself recorded: connection credential changes appear in the tenant's audit stream with who, when, and which connection, though never the material.
Storing secrets
All credential material lives in the tenant's secret store, keyed by connection, encrypted at rest, and readable only by the connector runtime executing under that tenant. The rules for connector authors are absolute, and marketplace review enforces them:
- Never accept a credential through
config. Theconfigrecord onConnectorCtxis for non-secret settings: subdomains, regions, queue names. It is visible in the workspace UI. - Never log material.
ctx.logoutput lands in traces and receipts people read. Log identifiers, not tokens. - Never persist a credential in connector state, cache it in module scope, or write it to disk. If a token needs refreshing, that is the platform's job.
- Never forward a credential to a third system. A connector authenticates to its one target; fan-out is an architecture smell and a review failure.
The full secret-store reference, including bring-your-own-KMS-key and access audit, is on secrets.
Choosing a pattern
| Target | Use | Why |
|---|---|---|
| SaaS API with OAuth apps | oauth2 | Scoped consent, automatic refresh, revocable by the vendor account owner. |
| SaaS API with account keys only | api_key | Simplest honest declaration; rely on staged rotation. |
| Anything inside AWS | aws_iam | No long-lived secret at all; the strongest available posture. |
| On-prem or building-management gateway | basic or mtls | Matches what the hardware actually speaks; mtls where certificates are provisioned. |
| Public read-only feed | none | Declare honestly; do not invent a key requirement. |
| Operator pack | none | Operators hold no credentials; they act through the connectors the tenant authorized. See the pack manifest. |
Keep going
- Secrets: the tenant secret store in full.
- Tools & auth: the declaration surface next to the tool contract.
- Connector manifest: where
authsits in the def. - Tenancy & isolation: why a connection's credentials can only ever serve one tenant.