OAuth and delegation
A tool often needs to act as the person who started the workflow: read their calendar, call an API with their token. Long Tail keeps each person’s OAuth tokens encrypted, hands tools a short-lived delegation token instead of the credential, and lets an external MCP server trade that token for exactly the access it was scoped for. This page covers OAuth sign-in, token storage, delegation tokens and service tokens.
Person signs in with OAuth ─▶ JWT issued, provider tokens stored encryptedWorkflow starts ───────────▶ envelope carries userIdActivity calls an MCP tool ▶ delegation token minted (scoped, 5-minute TTL), passed as _auth.tokenExternal server ───────────▶ validates the token, asks the delegation API for a fresh provider token ─▶ calls the external API as that personOAuth sign-in
Section titled “OAuth sign-in”Providers: Google (OpenID Connect with PKCE), GitHub (authorization code), Microsoft (Entra ID with PKCE) and a Mock provider for local testing.
OAUTH_ENCRYPTION_KEY=<64 hex characters> # 32 bytes, required for OAuth
OAUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.comOAUTH_GOOGLE_CLIENT_SECRET=your-client-secret
OAUTH_GITHUB_CLIENT_ID=your-github-client-idOAUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
OAUTH_MICROSOFT_CLIENT_ID=your-azure-app-idOAUTH_MICROSOFT_CLIENT_SECRET=your-azure-secretOAUTH_MICROSOFT_TENANT_ID=commonGenerate the key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"await start({ database: { connectionString: process.env.DATABASE_URL }, auth: { secret: process.env.JWT_SECRET, oauth: { encryptionKey: process.env.OAUTH_ENCRYPTION_KEY, autoProvision: true, // create the user on first sign-in (default true) defaultRoleType: 'member', providers: [ { provider: 'google', clientId: process.env.OAUTH_GOOGLE_CLIENT_ID!, clientSecret: process.env.OAUTH_GOOGLE_CLIENT_SECRET!, scopes: ['openid', 'email', 'profile'], }, ], }, },});A provider appears on the login page only when it is configured. Password login always works.
-
The login page reads
GET /api/auth/oauth/providers. -
“Sign in with Google” goes to
GET /api/auth/oauth/google, which sets CSRF state and a PKCE verifier and redirects to the provider. -
The provider returns to
GET /api/auth/oauth/google/callback. Long Tail exchanges the code and reads the user’s email and name. -
An existing user (matched by email or provider id) signs in. A new one is created when
autoProvisionis on. -
Provider tokens are encrypted with AES-256-GCM into
lt_oauth_tokens, one set per user per provider, refreshed automatically when the provider supports it. -
A Long Tail JWT is issued, the same as a password login.
People manage their connections on the Credentials page (/credentials). In server code, getFreshAccessToken(userId, provider) and listOAuthConnections(userId) read them.
Identity inside a tool
Section titled “Identity inside a tool”The authenticated user’s id rides the envelope into every activity. getToolContext() returns it wherever the code runs, as a proxy activity, an MCP tool or a YAML workflow worker:
import { getToolContext } from '@hotmeshio/long-tail';
const ctx = getToolContext();if (ctx) { ctx.principal.id; // user or bot id ctx.principal.type; // 'user' | 'bot' ctx.principal.roles; // ['reviewer', 'engineer'] ctx.credentials.delegationToken; // scoped JWT}A cron-triggered workflow has no user. Activities that need one should check and fail clearly.
Delegation tokens
Section titled “Delegation tokens”A delegation token is a JWT signed with JWT_SECRET that names one user, a set of scopes and a short lifetime:
{ "type": "delegation", "sub": "user-123", "scopes": ["oauth:google:read", "files:read"], "workflowId": "wf-abc", "serverId": "ext-server", "iss": "long-tail", "iat": 1711461600, "exp": 1711461900}When an activity calls an MCP tool, Long Tail reads the user from context, mints a token with the mcp:tool:call scope, and passes _auth: { userId, token } in the tool’s arguments. A tool that needs no auth ignores _auth. The MCP client derives _auth from the ambient context, so YAML workflow workers get it with no wiring.
In server code, createDelegationToken(userId, scopes, ttlSeconds, metadata) mints one (default 300 seconds, max 3600), and validateDelegationToken with requireScope checks one. Both live in services/auth/delegation.
| A delegation token | |
|---|---|
| Reaches | Only the resources its scopes name |
| Lives | 5 minutes by default, 1 hour at most |
| Refreshes | Never. Mint a new one. |
| Authenticates as | Its sub only |
| Reaches the admin API | Never |
The delegation API
Section titled “The delegation API”External MCP servers call these endpoints:
GET /api/delegation/oauth/:provider/tokenAuthorization: Bearer <delegation-token>Returns a fresh provider access token for the token’s user. Requires the scope oauth:<provider>:read:
{ "access_token": "ya29.a0AfH6...", "expires_at": "2024-03-26T18:00:00.000Z", "scopes": ["openid", "email", "profile"], "provider": "google"}POST /api/delegation/validateAuthorization: Bearer <service-token>Content-Type: application/json
{ "token": "<delegation-token>" }Returns the token’s claims, so an external server can verify a token without holding JWT_SECRET:
{ "valid": true, "userId": "user-123", "scopes": ["oauth:google:read"], "workflowId": "wf-abc", "expiresAt": "2024-03-26T17:05:00.000Z"}Service tokens
Section titled “Service tokens”An external MCP server authenticates to the delegation API with a service token: a long-lived key prefixed lt_svc_, stored as a bcrypt hash. Issue one in server code with database access:
const { id, rawToken } = await generateServiceToken( 'ext-calendar-server', // name serverId, // lt_mcp_servers id, or null ['delegation:validate'], // scopes);The raw token is returned once. listServiceTokens(serverId) lists a server’s tokens without hashes, and revokeServiceToken(id) deletes one.
The external server pattern
Section titled “The external server pattern”-
Register the server with Long Tail as a remote MCP server.
-
Give it a service token (the example reads
LT_SERVICE_TOKEN). -
Accept tool calls with
_auth.tokenin the arguments. -
Validate the delegation token with
POST /api/delegation/validate. -
Fetch the user’s provider token with
GET /api/delegation/oauth/:provider/token. -
Call the external API and return the result.
The repo’s examples/external-mcp-server/ is a working server, and docker-compose.external.yml runs it beside the main stack:
docker compose -f docker-compose.yml -f docker-compose.external.yml up -d --buildTest locally with the mock provider
Section titled “Test locally with the mock provider”The mock provider implements the authorization code flow and signs in a preconfigured test user.
docker compose --profile test up -d --buildOAUTH_ENCRYPTION_KEY=<64 hex characters>OAUTH_MOCK_CLIENT_ID=test-clientOAUTH_MOCK_CLIENT_SECRET=test-secretMOCK_OAUTH_AUTH_URL=http://mock-oauth:9080/authorizeMOCK_OAUTH_TOKEN_URL=http://mock-oauth:9080/tokenMOCK_OAUTH_USERINFO_URL=http://mock-oauth:9080/userinfoOpen http://localhost:3000/login and choose Sign in with Mock (Test). To exercise the chain, sign in with the password you set and start a workflow that calls MCP tools:
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \ -H 'Content-Type: application/json' \ -d "{\"username\":\"superadmin\",\"password\":\"$LT_PASSWORD\"}" | jq -r .token)
curl -s -X POST http://localhost:3000/api/insight \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"prompt":"List all files","wait":false}'Each tool call the workflow makes carries _auth.userId and a fresh delegation token.
Token types
Section titled “Token types”| Token | Lifetime | Scope | Created by | Used by |
|---|---|---|---|---|
| User JWT | 24 hours | Full API, RBAC-scoped | Login | Dashboard, API |
Bot API key (lt_bot_) |
Until revoked | Full API, RBAC-scoped | A builder | Scripts, CI, MCP clients |
| Delegation token | 5 minutes | Named scopes | Workflow activities | MCP tools, delegation API |
Service token (lt_svc_) |
Until revoked | Server-specific | Server code | External MCP servers |
| Provider access token | Provider-set | Provider scopes | OAuth flow | External APIs |
