Authentication Modes
How vendors authenticate with their backing service — declare a credential type once, the platform handles connection storage, token refresh, and runtime injection
A vendor declares what kind of credentials it needs by listing one or more entries under auth.credentials. The platform reads that declaration to:
- Render the right "Connect" UI when an organization administrator adds the integration (a Google-style redirect, a plain credentials form, or just the API-key field).
- Store the connection record encrypted, scoped to the organization (or workspace, for system-mode credentials).
- Inject a usable
ctx.authpayload into every tool execution so bundle code never touches raw secrets.
This page enumerates the seven supported credential types, when to pick each one, and what the bundle receives at execute time.
At a Glance
| Type | Flow | Spec | Bundle gets in ctx.auth | Typical use |
|---|---|---|---|---|
oauth | Authorization-code redirect (optional PKCE + refresh token) | RFC 6749 §4.1 (+ RFC 7636 for PKCE, RFC 6749 §6 for refresh) | { kind: "oauth", accessToken, refreshToken?, tokenType: "Bearer", expiresAt? } | User-facing OAuth — Google, Slack, Microsoft, GitHub for users |
oauthApp | OAuth app installation (admin consent) | RFC 6749 §4.1 (vendor-specific app-install pattern layered on top) | { kind: "oauth", accessToken, expiresAt? } | GitHub Apps, Slack apps, Atlassian apps — installed once per workspace |
oauthClientCredentials | Client-credentials grant (no redirect) | RFC 6749 §4.4 (+ §2.3.1 for useBasicAuth) | { kind: "oauth", accessToken, tokenType: "Bearer", expiresAt } | SAP S/4HANA XSUAA, Auth0 M2M, Okta service apps, Microsoft Graph in System Mode |
apiKey | Single static key in header or query param | No formal RFC (convention; RFC 6750 when headerPrefix: "Bearer") | { kind: "apiKey", apiKey: "<key>" } | Stripe, OpenAI, Anthropic, most REST APIs |
basicAuth | Username + password (no redirect) | RFC 7617 | Account-delivered: { kind: "oauth", accessToken: "<base64(user:pass)>", tokenType: "Basic" } · Connection-delivered raw pair: { kind: "basicAuth", username, password } | Legacy on-prem APIs, SAP communication users, Jira Server, Confluence Server |
botToken | Static bot/service token | No formal RFC (vendor convention; RFC 6750 when headerPrefix: "Bearer") | { kind: "botToken", botToken: "<token>" } | Slack bot tokens, Discord bot tokens, Telegram bots |
none | No auth | — | { kind: "none" } | Local-only providers (Ollama, vLLM), public APIs |
ctx.auth is a kind-discriminated union — always narrow on ctx.auth.kind before reading credential fields. Use the narrowing helpers from @scrydon/sdk-authoring/integrations/context (requireOAuthToken, requireApiKey, requireBasicAuth, requireBotToken) to get the credential value with a clear error on kind mismatch. The platform pre-resolves whichever flow the credential declares — bundles never implement OAuth redirects, refresh logic, or basic-auth encoding themselves.
Picking a Type
Run through these questions in order:
- Does the user log in interactively to the vendor? →
oauth. - Does the vendor want one record per workspace via app-install (no per-user redirect)? →
oauthApp. - Does the vendor expose machine-to-machine OAuth (clientId + clientSecret → token, no user)? →
oauthClientCredentials. - Does the vendor want a single API key in a header or query param? →
apiKey. - Does the vendor accept HTTP Basic auth with a username + password? →
basicAuth. - Does the vendor issue a static bot/service token (Slack-style
xoxb-...)? →botToken. - Is there no auth at all (e.g. self-hosted Ollama)? →
none.
A vendor can declare more than one. Examples:
- A SaaS that offers both interactive OAuth and a bot token: declare both, the user picks at connection time.
- A vendor that runs against either Cloud (OAuth client-credentials) or on-prem (HTTP Basic): declare both, gate visibility on a
deploymentTypeconfigField.
Authoring Examples
oauth — Authorization-code flow
For interactive user logins. The platform handles the redirect, PKCE, refresh-token rotation, and storage.
auth: {
credentials: {
oauth: {
type: "oauth",
label: "Google Account",
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: "https://oauth2.googleapis.com/token",
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo",
scopes: ["openid", "email", "https://www.googleapis.com/auth/drive.readonly"],
pkce: true,
accessType: "offline",
prompt: "consent",
supportsRefreshTokenRotation: true,
},
},
default: "oauth",
},Tenant-scoped URLs are templated against vendor.configFields:
auth: {
credentials: {
oauth: {
type: "oauth",
label: "Microsoft Account",
authorizationUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
tokenUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token",
requiredConfig: ["tenantId"],
// ...
},
},
},
configFields: [
{ key: "tenantId", label: "Azure AD Tenant ID", required: true, type: "text" },
],oauthApp — OAuth App installation
For app-install flows (one record per workspace, not per user). Same shape as oauth but no user redirect — the app is installed by an admin once.
auth: {
credentials: {
app: {
type: "oauthApp",
label: "GitHub App",
authorizationUrl: "https://github.com/apps/scrydon-bot/installations/new",
tokenUrl: "https://api.github.com/app/installations/{installationId}/access_tokens",
scopes: [],
pkce: false,
},
},
default: "app",
},oauthClientCredentials — Machine-to-machine
RFC 6749 §4.4. The user supplies clientId + clientSecret once at connection time, the platform exchanges them for an access token whenever needed and caches it until expiry. No user redirect.
auth: {
credentials: {
oauth: {
type: "oauthClientCredentials",
label: "Auth0 Service Account",
tokenUrl: "https://example.auth0.com/oauth/token",
scopes: ["read:users", "create:users"],
audience: "https://api.example.com",
useBasicAuth: false,
},
},
default: "oauth",
},For per-tenant token endpoints (the SAP S/4HANA XSUAA pattern), template the tokenUrl against configFields:
auth: {
credentials: {
oauth: {
type: "oauthClientCredentials",
label: "SAP S/4HANA Service Account",
tokenUrl: "https://{subdomain}.authentication.{region}.hana.ondemand.com/oauth/token",
requiredConfig: ["subdomain", "region"],
useBasicAuth: true,
},
},
default: "oauth",
},
configFields: [
{ key: "subdomain", label: "BTP Subdomain", required: true, type: "text" },
{ key: "region", label: "BTP Region", required: true, type: "text" },
],| Field | Purpose |
|---|---|
tokenUrl | Token endpoint. May contain {configKey} placeholders. |
requiredConfig | configField keys substituted into tokenUrl at fetch time. Each must appear in vendor.configFields. |
scopes | Space-separated scopes requested in the token call. |
useBasicAuth | RFC 6749 §2.3.1 — when true (default), clientId:clientSecret are sent via Authorization: Basic <b64> on the token request; when false, in the form body. Some IdPs reject one or the other. |
audience | Required by Auth0, Okta, and several custom IdPs. |
additionalBodyParams | Vendor-specific extras posted alongside grant_type=client_credentials. |
apiKey — Single static key
auth: {
credentials: {
apiKey: {
type: "apiKey",
label: "API Key",
headerName: "Authorization",
headerPrefix: "Bearer",
},
},
default: "apiKey",
},headerName, headerPrefix, and queryParam are all optional — leave the prefix off for raw-token APIs, or use queryParam: "api_key" for query-string-only APIs.
At runtime, tools receive { kind: "apiKey", apiKey: "<key>" }. Read it with requireApiKey(ctx.auth) or by narrowing on kind:
import { requireApiKey } from "@scrydon/sdk-authoring/integrations/context";
async execute(input, ctx) {
const apiKey = requireApiKey(ctx.auth); // throws CredentialKindError on mismatch
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${apiKey}` },
});
// ...
}Standalone canvas blocks
API-key integrations work as standalone canvas blocks with no per-block credential picker. The platform automatically resolves the org's configured connection for the executing tool's own vendor — no user action is needed at block-placement time. Resolution is environment-scoped: the connection matching the current workspace environment wins, with a wildcard fallback. It is also structurally scoped: a tool can only ever receive its own integration's credential, never a credential from a different vendor. ctx.secrets remains the channel for non-auth configured secrets (extras declared in vendor.secrets[] such as a search-engine ID) and is never used for the primary credential.
basicAuth — Username + password
auth: {
credentials: {
basic: {
type: "basicAuth",
label: "Communication User",
description: "SAP communication user with password",
},
},
default: "basic",
},At runtime, ctx.auth arrives in one of two shapes depending on how the connection was delivered:
- Account-delivered (platform pre-encodes the pair):
{ kind: "oauth", accessToken: "<base64(user:pass)>", tokenType: "Basic" }. PassaccessTokenandtokenTypedirectly into theAuthorizationheader — the encoding is done for you. - Connection-delivered raw pair:
{ kind: "basicAuth", username, password }. UserequireBasicAuth(ctx.auth)(orctx.auth.kind === "basicAuth") and encode the header yourself.
Tools that handle basicAuth vendors should narrow on kind to accept both shapes:
import { requireBasicAuth } from "@scrydon/sdk-authoring/integrations/context";
async execute(input, ctx) {
let authHeader: string;
if (ctx.auth.kind === "oauth" && ctx.auth.tokenType === "Basic") {
// Account-delivered: platform already encoded the pair
authHeader = `Basic ${ctx.auth.accessToken}`;
} else {
// Connection-delivered raw pair
const { username, password } = requireBasicAuth(ctx.auth);
authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
}
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: authHeader },
});
// ...
}For non-standard headers (a few APIs use X-Auth-Token or a non-Basic scheme), override the header name and prefix in the credential declaration:
{
type: "basicAuth",
label: "Custom Header",
headerName: "X-Auth-Token",
headerPrefix: "Token",
},botToken — Static bot/service token
auth: {
credentials: {
bot: {
type: "botToken",
label: "Slack Bot Token",
headerName: "Authorization",
headerPrefix: "Bearer",
},
},
default: "bot",
},At runtime, tools receive { kind: "botToken", botToken: "<token>" }. Use requireBotToken(ctx.auth) or narrow on kind:
import { requireBotToken } from "@scrydon/sdk-authoring/integrations/context";
async execute(input, ctx) {
const token = requireBotToken(ctx.auth);
const response = await fetch("https://slack.com/api/conversations.list", {
headers: { Authorization: `Bearer ${token}` },
});
// ...
}none — No auth
auth: {
credentials: {
none: { type: "none" },
},
default: "none",
},At runtime, tools receive { kind: "none" }. Guard against accidental credential reads by narrowing:
async execute(input, ctx) {
// No credential — proceed without Authorization header
if (ctx.auth.kind !== "none") {
throw new Error("Expected no-auth credential");
}
const response = await fetch("https://api.example.com/public/data");
// ...
}Bundle Code Pattern
ctx.auth is a kind-discriminated union. Import the narrowing helpers from @scrydon/sdk-authoring/integrations/context and narrow on kind before reading credential fields:
import {
requireOAuthToken,
requireApiKey,
requireBasicAuth,
requireBotToken,
} from "@scrydon/sdk-authoring/integrations/context";
async execute(input, ctx) {
const headers: Record<string, string> = { Accept: "application/json" };
switch (ctx.auth.kind) {
case "oauth":
// Covers oauth, oauthApp, oauthClientCredentials, and account-delivered basicAuth.
// ctx.auth.tokenType is "Bearer" for OAuth flows and "Basic" for basicAuth accounts.
headers.Authorization = ctx.auth.tokenType
? `${ctx.auth.tokenType} ${ctx.auth.accessToken}`
: ctx.auth.accessToken;
break;
case "apiKey":
// The key is now in ctx.auth.apiKey — not accessToken.
headers.Authorization = `Bearer ${ctx.auth.apiKey}`;
break;
case "basicAuth": {
// Connection-delivered raw pair — encode it yourself.
const { username, password } = ctx.auth;
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
break;
}
case "botToken":
headers.Authorization = `Bearer ${ctx.auth.botToken}`;
break;
case "none":
// No credential — do not set an Authorization header.
break;
}
const response = await fetch(`${ctx.config.baseUrl}/items`, { headers });
// ...
}For single-kind tools, the require* helpers are more concise and throw a CredentialKindError with an actionable message on kind mismatch:
import { requireApiKey } from "@scrydon/sdk-authoring/integrations/context";
async execute(input, ctx) {
const apiKey = requireApiKey(ctx.auth);
// ...
}For OAuth flows the platform refreshes expired tokens before invoking the actor — bundles never see an expired accessToken. For oauthClientCredentials the platform re-exchanges clientId:clientSecret when the cached token is within 60 seconds of expiry.
Migrating from v2 — ctx.auth is now a kind-discriminated union, not a flat object.
- API key tools must switch from
ctx.auth.accessTokentoctx.auth.apiKey(orrequireApiKey(ctx.auth)). ReadingaccessTokenfor an API key credential will produceundefinedat runtime. - OAuth tools retain
accessTokenunder the"oauth"kind variant and continue working, but should narrow onctx.auth.kind === "oauth"going forward to benefit from type safety. - Bot token tools must switch from
ctx.auth.accessTokentoctx.auth.botToken(orrequireBotToken(ctx.auth)). nonetools previously received{ accessToken: "" }— they now receive{ kind: "none" }with noaccessTokenfield at all.
Block-Level authMode
Each block under a product declares which credential it consumes via authMode. The string must match a key in the vendor's auth.credentials (or one of the canonical mode strings — api_key, oauth, oauth_app, oauth_client_credentials, basic_auth, bot_token, none).
const myBlock = defineBlock({
type: "my_block",
authMode: "oauth", // matches auth.credentials.oauth above
// ...
});When a vendor declares multiple credential types, each block can pick its own — the workflow editor prompts the user to connect the right one.
See Also
- Getting Started — write your first vendor
- SDK Reference — full
defineVendor/defineProductAPI - Capabilities — LLM, STT, TTS, embedding, and other runtime capabilities