MCP Fusion/Security and governance/Authentication

Authentication

Ask AI about Vinkius

Three auth packages for connectors: JWT verification with JWKS, timing-safe API keys, and the OAuth 2.0 device authorization flow turned into tools the agent can drive.

A connector that acts on someone's account must know who is asking. MCP Fusion ships three auth packages, each a middleware plus an optional auth tool, so the agent can be blocked, challenged and then unblocked without your handler knowing the difference.

Where identity comes from

Auth reads identity from ctx, and ctx is built per request by contextFactory. The canonical wiring:

typescript
registry.attachToServer(server, {
  contextFactory: async (extra) => ({
    token: extra.session?.authToken ?? '',
    headers: extra.headers ?? {},
  }),
});

Everything below reads that context.

JWT

bash
npm install @mcpfusion/jwt
typescript
import { requireJwt } from '@mcpfusion/jwt';

f.query('billing.list_invoices')
  .use(requireJwt({
    jwksUri: 'https://auth.example.com/.well-known/jwks.json',
    issuer: 'https://auth.example.com/',
    audience: 'my-connector',
    requiredClaims: ['sub', 'tenant_id'],
    onVerified: (ctx, payload) => {
      ctx.tenantId = payload.tenant_id;   // derived identity for the handler
    },
  }))
  .handle(async (input, ctx) => listInvoices(ctx.tenantId));

JwtVerifier accepts secret (HS256), publicKey (PEM) or jwksUri (remote key set, cached per verifier). With jose installed, RS256, ES256 and JWKS all work; without it, a native HS256 path verifies with timingSafeEqual and refuses any other algorithm. issuer, audience, clockTolerance (60 seconds) and requiredClaims are validated on every token. verifyDetailed() returns { valid, payload, reason } when you want the reason instead of null.

Default token extraction order: ctx.token, then ctx.jwt, then ctx.headers.authorization (the Bearer prefix is stripped). Failures return a self-healing toolError with code JWT_INVALID and an auth action to recover.

The JWT package verifies tokens, it does not refresh them. There is no refresh-token support in the source, so a connector that needs long-lived provider access should use the OAuth flow below or the credential vault.

The optional createJwtAuthTool() exposes verify and status as agent-callable actions when the client itself holds the token.

API keys

bash
npm install @mcpfusion/api-key
typescript
import { requireApiKey } from '@mcpfusion/api-key';

f.mutation('billing.refund')
  .use(requireApiKey({
    keys: [process.env.SERVICE_KEY!],
    onValidated: (ctx) => { ctx.service = 'billing-service'; },
  }))
  .handle(...);

Keys can be plaintext (hashed internally with SHA-256 at construction) or pre-hashed, or validated by your async validator function. Order of checks: non-empty, minLength (16 by default), optional prefix, then the validator or the hash set. Extraction order: ctx.apiKey, ctx.headers['x-api-key'], then ctx.headers.authorization (ApiKey and Bearer prefixes stripped).

The comparison is constant-time in intent: the manager compares hashes in a way that does not short-circuit on the first differing byte. Do not treat the key length as a secret.

OAuth device flow for the agent

When the connector needs an account the agent does not have yet, the flow must be drivable from a chat: the server hands over a code and a URL, the human approves in a browser, the agent asks again. That is RFC 8628, and the package turns it into tools:

bash
npm install @mcpfusion/oauth
typescript
import { createAuthTool, requireAuth } from '@mcpfusion/oauth';

const auth = createAuthTool({
  clientId: process.env.OAUTH_CLIENT_ID!,
  authorizationEndpoint: 'https://auth.example.com/device/code',
  tokenEndpoint: 'https://auth.example.com/device/token',
  onAuthenticated: (token) => { /* cache or forward the token */ },
});

f.query('analytics.report')
  .use(requireAuth())          // blocks with AUTH_REQUIRED until a token exists
  .handle(...);

The tool exposes four actions:

ActionBehavior
loginrequests a device code, returns the verification URL and the code
completeexchanges the code once; authorization_pending means "not yet, ask again"
statusreports whether a token exists, and optionally which user it belongs to
logoutclears the token

The polling loop honors RFC 8628: the first poll is immediate, authorization_pending continues, slow_down adds five seconds, any other error terminates with the provider's own description, and the deadline throws "Device authorization expired. Start a new flow." The verifier does not send a scope for you and there is no automatic refresh: the token store is a file under the home directory (.mcpfusion/token.json, mode 0600, with an icacls fallback on Windows), so treat it as the single-user local default and wire your own persistence for a hosted connector.

requireAuth() checks for the presence of a token, not its validity. Pair it with requireJwt when the upstream token must be cryptographically verified.

Production notes

  • Prefer platform credentials over user tokens for server-to-server calls: declare them with defineCredentials and read them with requireCredential. See Credentials.
  • On Vinkius Cloud, invoke with a connection token and use Connection Tokens in the console to revoke access per client.
  • Auth middleware composes with the rest of the chain: put it outermost so unauthorized calls never reach validation, as shown in Middleware and context.

Next steps