Agent Apps Docs

Authentication

Configure OAuth 2.0 and API key authentication for your app, and access tokens at runtime.

Authentication

Most apps need credentials to talk to external services. The Agent Apps SDK provides two main authentication strategies — OAuth 2.0 for delegated user flows, and API key for static secrets — plus a set of runtime helpers for reading tokens, checking auth status, and refreshing credentials.

Auth types at a glance

Typeauth.typeUse case
None"none" or falseApp requires no authentication
API key"api-key"Static secrets (personal tokens, service keys)
OAuth 2.0"oauth2"Delegated user flows (Google, Microsoft, GitHub, etc.)
GitHub App"github-app"GitHub App installations with optional user OAuth

When auth is omitted or set to false, the app has no authentication configuration and ctx.requestAuth() will reject.


Declaring auth in the manifest

The auth field in agentapp.json declares which strategy your app uses. The host uses this to build the right configuration UI at install time and to wire up the correct callbacks at runtime.

API key

Best for services that issue static credentials — personal access tokens, service account keys, or simple API keys.

{
  "auth": {
    "type": "api-key",
    "systemSecrets": ["OPENAI_API_KEY"],
    "userSecrets": ["GITHUB_TOKEN"],
    "headerName": "Authorization",
    "headerPrefix": "Bearer "
  }
}
FieldDefaultDescription
systemSecrets[]App-wide secrets shared across all installations (service keys)
userSecrets[]Per-user secrets the user configures after install (personal tokens)
headerName"Authorization"HTTP header name the host uses when injecting the key
headerPrefix"Bearer "Prefix prepended to the secret value in the header

Fail-fast behavior: when userSecrets are declared but not yet configured by the user, the SDK rejects every tool call with a clear UNAUTHORIZED error — your handler never runs without credentials.

OAuth 2.0

For services that support delegated authorization (Google, Microsoft, Slack, etc.).

{
  "auth": {
    "type": "oauth2",
    "provider": "google",
    "authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth",
    "tokenUrl": "https://oauth2.googleapis.com/token",
    "scopes": ["https://www.googleapis.com/auth/gmail.readonly"],
    "optionalScopes": ["https://www.googleapis.com/auth/gmail.send"],
    "pkce": true,
    "systemSecrets": ["CLIENT_ID", "CLIENT_SECRET"],
    "userSecrets": ["ACCESS_TOKEN", "REFRESH_TOKEN"],
    "extraFields": [
      {
        "name": "TEAM_ID",
        "label": "Slack Team ID",
        "type": "text",
        "required": true,
        "placeholder": "T01234ABC",
        "hint": "Found in your Slack workspace settings"
      }
    ]
  }
}
FieldRequiredDescription
provideryesOAuth provider identifier (e.g. "google", "microsoft")
authorizeUrlyesOAuth2 authorization endpoint URL
tokenUrlyesOAuth2 token exchange endpoint URL
scopesyesRequired scopes (at least one)
optionalScopesnoScopes the user can opt into
scopeSeparatornoSeparator used to join scopes (defaults to space)
pkcenoEnable PKCE for public clients without client_secret
systemSecretsnoSystem-level secrets (e.g. CLIENT_ID, CLIENT_SECRET)
userSecretsnoUser-level secrets (e.g. ACCESS_TOKEN, REFRESH_TOKEN)
extraFieldsnoExtra user-configurable fields shown at install time

GitHub App

A specialized flow for GitHub Apps that supports installation tokens and optional user OAuth.

{
  "auth": {
    "type": "github-app",
    "provider": "github",
    "appSlug": "my-github-app",
    "setupUrl": "https://github.com/apps/my-github-app/installations/new",
    "userOAuth": true,
    "tokenUrl": "https://github.com/login/oauth/access_token",
    "permissions": {
      "issues": "write",
      "pull_requests": "read",
      "contents": "read"
    },
    "events": ["issues", "pull_request"],
    "systemSecrets": ["GITHUB_APP_ID", "PRIVATE_KEY", "WEBHOOK_SECRET"],
    "userSecrets": ["INSTALLATION_ID"]
  }
}

When userOAuth is true, both tokenUrl and at least one events entry are required.


OAuth flow from your app

ctx.requestAuth()

Initiates an OAuth flow by asking the host to open a browser, complete the authorization, and store the resulting tokens.

// Request OAuth authentication — the host handles the browser flow
const result = await ctx.requestAuth({
  provider: 'google',
  scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
  timeoutMs: 300_000, // optional, defaults to 5 minutes
});

// result: { accessToken, refreshToken?, expiresAt? }
console.log(result.accessToken);

The provider must match the one declared in the manifest's auth.provider field. If the app's auth is not oauth2 (or is false/none/api-key), requestAuth() throws.

ctx.auth.getStatus()

Check whether the user is currently authenticated without triggering a flow.

const status = await ctx.auth.getStatus();

// status: {
//   authenticated: boolean;
//   provider?: string;
//   scopes?: string[];
//   expiresAt?: string;
//   authRequired?: boolean;
//   authUrl?: string;
// }

if (!status.authenticated) {
  // Prompt the user to authenticate
  await ctx.requestAuth({ provider: 'google' });
}

ctx.auth.getUrl()

Get just the authorization URL without completing the flow. Useful when you need to display the link in a custom UI.

const { url, state } = await ctx.auth.getUrl('google', [
  'https://www.googleapis.com/auth/gmail.readonly',
]);

// Open the URL in the user's browser
await ctx.openUrl({ url, title: 'Connect your Google account' });

ctx.auth.refresh()

Force a token refresh using the host-stored refresh token. The host handles the exchange and persists the new credentials — no client secrets in your app code.

try {
  const refreshed = await ctx.auth.refresh();
  console.log('New access token:', refreshed.accessToken);
} catch (err) {
  // Refresh failed — the user may need to re-authenticate
  console.error('Token refresh failed:', err.message);
}

API keys and manual secrets

ctx.secrets.getUser()

Retrieve all user-level secrets as a key-value map.

const userSecrets = await ctx.secrets.getUser();
const apiKey = userSecrets?.['GITHUB_TOKEN'];

if (!apiKey) {
  throw new Error('GitHub token not configured. Please set it in your app settings.');
}

ctx.userSecret(name)

Convenience method to read a single user secret by name.

const apiKey = await ctx.userSecret('GITHUB_TOKEN');
if (!apiKey) {
  throw new Error('GitHub token not configured');
}

// Use the token in an API call
const response = await fetch('https://api.github.com/user', {
  headers: { Authorization: `Bearer ${apiKey}` },
});

ctx.secrets.getSystem()

Retrieve system-level secrets (shared across all installations of this app).

const systemSecrets = await ctx.secrets.getSystem();
const clientId = systemSecrets?.['CLIENT_ID'];
const clientSecret = systemSecrets?.['CLIENT_SECRET'];

ctx.systemSecret(name)

Convenience method to read a single system secret by name.

const clientId = await ctx.systemSecret('CLIENT_ID');

ctx.secrets.updateUser(secrets)

Update user-level secrets programmatically — for example, after refreshing a token inside your app.

// After obtaining a new token through your own refresh logic
await ctx.secrets.updateUser({
  ACCESS_TOKEN: newAccessToken,
  REFRESH_TOKEN: newRefreshToken,
});

ctx.requestSecret()

Prompt the user to provide a secret value interactively. The host shows a UI for the user to enter the value.

const result = await ctx.requestSecret({
  name: 'STRIPE_API_KEY',
  description: 'Enter your Stripe secret API key (starts with sk_live_)',
  timeoutMs: 300_000,
});

console.log('Secret received:', result.value);

Complete examples

OAuth 2.0 app (Google Calendar)

import { createApp } from '@agentapps/sdk';

export default createApp({
  id: 'app.google.calendar',
  version: '1.0.0',
  name: 'Google Calendar',
  description: 'View and manage Google Calendar events',
  // ... other required fields
  auth: {
    type: 'oauth2',
    provider: 'google',
    authorizeUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
    tokenUrl: 'https://oauth2.googleapis.com/token',
    scopes: ['https://www.googleapis.com/auth/calendar'],
    pkce: true,
    systemSecrets: ['CLIENT_ID', 'CLIENT_SECRET'],
    userSecrets: ['ACCESS_TOKEN', 'REFRESH_TOKEN'],
  },
  runtime: {
    transport: 'http',
    teros: { containerMode: 'process' },
  },
}, (app) => {
  app.tool({
    name: 'list_events',
    description: 'List upcoming calendar events',
    parameters: {
      type: 'object',
      properties: {
        maxResults: { type: 'number', description: 'Maximum number of events to return' },
      },
    },
    handler: async (args, ctx) => {
      // Check auth status first
      const status = await ctx.auth.getStatus();
      if (!status.authenticated) {
        // Trigger the OAuth flow — opens browser, stores tokens
        await ctx.requestAuth({ provider: 'google' });
      }

      // Get the stored access token
      const userSecrets = await ctx.secrets.getUser();
      const accessToken = userSecrets?.['ACCESS_TOKEN'];
      if (!accessToken) {
        throw new Error('No access token found. Please re-authenticate.');
      }

      const maxResults = (args.maxResults as number) ?? 10;
      const response = await fetch(
        `https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=${maxResults}`,
        { headers: { Authorization: `Bearer ${accessToken}` } },
      );

      if (response.status === 401) {
        // Token expired — try refreshing
        const refreshed = await ctx.auth.refresh();
        const retryResponse = await fetch(
          `https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=${maxResults}`,
          { headers: { Authorization: `Bearer ${refreshed.accessToken}` } },
        );
        return await retryResponse.json();
      }

      return await response.json();
    },
  });
});

API key app (GitHub)

import { createApp } from '@agentapps/sdk';

export default createApp({
  id: 'app.github.issues',
  version: '1.0.0',
  name: 'GitHub Issues',
  description: 'View and manage GitHub issues',
  // ... other required fields
  auth: {
    type: 'api-key',
    userSecrets: ['GITHUB_TOKEN'],
    headerName: 'Authorization',
    headerPrefix: 'Bearer ',
  },
  runtime: {
    transport: 'http',
    teros: { containerMode: 'process' },
  },
}, (app) => {
  app.tool({
    name: 'list_issues',
    description: 'List open issues in a repository',
    parameters: {
      type: 'object',
      properties: {
        owner: { type: 'string', description: 'Repository owner' },
        repo: { type: 'string', description: 'Repository name' },
      },
      required: ['owner', 'repo'],
    },
    handler: async (args, ctx) => {
      // ctx.userSecret() is the simplest way to read a single secret
      const token = await ctx.userSecret('GITHUB_TOKEN');
      if (!token) {
        throw new Error('GitHub token not configured. Set GITHUB_TOKEN in your app settings.');
      }

      const owner = args.owner as string;
      const repo = args.repo as string;

      const response = await fetch(
        `https://api.github.com/repos/${owner}/${repo}/issues?state=open`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
            Accept: 'application/vnd.github.v3+json',
          },
        },
      );

      if (!response.ok) {
        throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
      }

      const issues = await response.json();
      return issues.map((issue: any) => ({
        number: issue.number,
        title: issue.title,
        state: issue.state,
        url: issue.html_url,
      }));
    },
  });
});

Health checks with auth

Use the onHealthCheck handler to verify credentials are present and report auth issues to the host.

import { createApp } from '@agentapps/sdk';

export default createApp({
  // ...
  auth: {
    type: 'oauth2',
    provider: 'google',
    // ...
  },
}, (app) => {
  app.healthCheck(async (ctx) => {
    const status = await ctx.auth.getStatus();

    if (!status.authenticated) {
      return {
        status: 'not_ready',
        message: 'Google authentication required',
        issues: [{
          type: 'auth_required',
          message: 'Connect your Google account to start using this app',
          action: 'authenticate',
        }],
      };
    }

    if (status.expiresAt) {
      const expiresAt = new Date(status.expiresAt);
      const now = new Date();
      const fiveMinutes = 5 * 60 * 1000;

      if (expiresAt.getTime() - now.getTime() < fiveMinutes) {
        // Proactively refresh
        try {
          await ctx.auth.refresh();
        } catch {
          return {
            status: 'degraded',
            message: 'Token expiring soon and refresh failed',
          };
        }
      }
    }

    return { status: 'ready' };
  });
});

Best practices

Never hardcode tokens

// ❌ Never do this
const TOKEN = 'ghp_xxxxxxxxxxxx';

// ✅ Always read from the host-managed secret store
const token = await ctx.userSecret('GITHUB_TOKEN');

Secrets are injected by the host at runtime — your app never sees the raw credential at build time, and they are never logged or serialized into tool results.

Always check auth status before API calls

// ✅ Check first, then request auth if needed
const status = await ctx.auth.getStatus();
if (!status.authenticated) {
  await ctx.requestAuth({ provider: 'google' });
}

Use ctx.auth.refresh() instead of manual refresh logic

The host stores refresh tokens and handles the exchange centrally. Calling ctx.auth.refresh() avoids race conditions between concurrent instances and keeps client secrets out of your app code.

Handle expired tokens gracefully

const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });

if (response.status === 401) {
  // Try refreshing once
  const refreshed = await ctx.auth.refresh();
  // Retry with the new token
  return await fetch(url, {
    headers: { Authorization: `Bearer ${refreshed.accessToken}` },
  });
}

Declare all secrets in the manifest

Secrets not declared in auth.systemSecrets or auth.userSecrets won't be available at runtime. The host only injects declared secrets.

{
  "auth": {
    "type": "api-key",
    "userSecrets": ["GITHUB_TOKEN", "GITHUB_ORG"]
  }
}

Use systemSecrets for shared credentials, userSecrets for per-user

  • systemSecrets — shared across all installations (e.g. CLIENT_ID, CLIENT_SECRET). Configured once by the app author or admin.
  • userSecrets — unique per installation (e.g. ACCESS_TOKEN, personal API keys). Configured by each user after install.

Next steps

  • Implement tools — add tools that use authenticated APIs
  • Storage — persist data scoped to your installation
  • Events — emit and receive events