Scrydon
Integrations

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:

  1. 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).
  2. Store the connection record encrypted, scoped to the organization (or workspace, for system-mode credentials).
  3. Inject a usable ctx.auth payload 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

TypeFlowSpecBundle gets in ctx.authTypical use
oauthAuthorization-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
oauthAppOAuth 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
oauthClientCredentialsClient-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
apiKeySingle static key in header or query paramNo formal RFC (convention; RFC 6750 when headerPrefix: "Bearer"){ kind: "apiKey", apiKey: "<key>" }Stripe, OpenAI, Anthropic, most REST APIs
basicAuthUsername + password (no redirect)RFC 7617Account-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
botTokenStatic bot/service tokenNo formal RFC (vendor convention; RFC 6750 when headerPrefix: "Bearer"){ kind: "botToken", botToken: "<token>" }Slack bot tokens, Discord bot tokens, Telegram bots
noneNo 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:

  1. Does the user log in interactively to the vendor?oauth.
  2. Does the vendor want one record per workspace via app-install (no per-user redirect)?oauthApp.
  3. Does the vendor expose machine-to-machine OAuth (clientId + clientSecret → token, no user)?oauthClientCredentials.
  4. Does the vendor want a single API key in a header or query param?apiKey.
  5. Does the vendor accept HTTP Basic auth with a username + password?basicAuth.
  6. Does the vendor issue a static bot/service token (Slack-style xoxb-...)?botToken.
  7. 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 deploymentType configField.

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" },
],
FieldPurpose
tokenUrlToken endpoint. May contain {configKey} placeholders.
requiredConfigconfigField keys substituted into tokenUrl at fetch time. Each must appear in vendor.configFields.
scopesSpace-separated scopes requested in the token call.
useBasicAuthRFC 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.
audienceRequired by Auth0, Okta, and several custom IdPs.
additionalBodyParamsVendor-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" }. Pass accessToken and tokenType directly into the Authorization header — the encoding is done for you.
  • Connection-delivered raw pair: { kind: "basicAuth", username, password }. Use requireBasicAuth(ctx.auth) (or ctx.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 v2ctx.auth is now a kind-discriminated union, not a flat object.

  • API key tools must switch from ctx.auth.accessToken to ctx.auth.apiKey (or requireApiKey(ctx.auth)). Reading accessToken for an API key credential will produce undefined at runtime.
  • OAuth tools retain accessToken under the "oauth" kind variant and continue working, but should narrow on ctx.auth.kind === "oauth" going forward to benefit from type safety.
  • Bot token tools must switch from ctx.auth.accessToken to ctx.auth.botToken (or requireBotToken(ctx.auth)).
  • none tools previously received { accessToken: "" } — they now receive { kind: "none" } with no accessToken field 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

On this page

On this page