Skip to content

Authentication ​

Secure your API with built-in authentication providers.

Changed in 0.3.0

createJWTAuthProvider now verifies the signature (it used to read the claims without checking it), createAgentServer throws when auth.enabled is true without a provider, generated tokens are 256-bit random values, and listing needs a user. See Upgrading to 0.3.0.

Which one to use ​

You haveUse
a handful of service keyscreateTokenAuthProvider({ tokens })
API keys in your own databasecreateTokenAuthProvider({ validateFn })
tokens signed with a key you hold (HS*, RS*, ES*)createJWTAuthProvider
an identity provider with rotating keys (JWKS), or your own sessionan AuthProvider on a JWT library, or resolveContext (cookies, bearer tokens you verify yourself)

Token-Based Authentication ​

Simple API key authentication for internal services.

typescript
import { createTokenAuthProvider } from '@cognipeer/agent-server';

const authProvider = createTokenAuthProvider({
  // Static token mapping: token -> userId
  tokens: {
    'api-key-1': 'user-1',
    'api-key-2': 'user-2',
    [process.env.ADMIN_API_KEY]: 'admin',
  },
});

const agentServer = createAgentServer({
  // ...
  auth: {
    enabled: true,
    provider: authProvider,
    headerName: 'Authorization',     // Default
    tokenPrefix: 'Bearer ',          // Default
    excludeRoutes: ['/docs', '/docs/*'],
  },
});

When validateFn is set it is asked first and decides alone: the static tokens are not consulted. To accept both, look the static map up inside your validateFn.

Dynamic Token Validation ​

typescript
const authProvider = createTokenAuthProvider({
  // Custom validation function
  validateFn: async (token) => {
    const user = await database.findUserByApiKey(token);
    
    if (user) {
      return {
        valid: true,
        userId: user.id,
      };
    }
    
    return {
      valid: false,
      error: 'Invalid API key',
    };
  },
});

JWT Authentication ​

typescript
import { createJWTAuthProvider } from '@cognipeer/agent-server';

const authProvider = createJWTAuthProvider({
  secret: process.env.JWT_SECRET!,
  algorithm: 'HS256',              // Default; HS256/384/512 (secret), RS256/384/512 and ES256/384/512 (the PEM public key as the secret)
  issuer: 'my-app',                // Optional - check the issuer
  audience: 'my-api',              // Optional - check the audience (a string or a list; the token's `aud` may be either)
  extractUserId: (payload) => payload.sub as string,   // Default: sub, then userId, then user_id (a non-empty string)
});

The provider verifies the signature with the key you configure, then checks exp, nbf, iss and aud. The key type comes from the configuration and never from the token: a token that says alg: "none", names any algorithm other than the configured one, or is signed with another key is refused. A secret is required (creating the provider without one throws). A token without exp is accepted unless you set requireExp: true; clockToleranceSec allows for clock drift.

No JWKS

The built-in provider has one static key: it does not fetch keys, so it cannot follow a key rotation. For an identity provider that publishes a JWKS, verify the token in front of the server (a gateway, an identity-aware proxy), or write an AuthProvider that verifies it with a JWT library such as jose:

typescript
import { createRemoteJWKSet, jwtVerify } from 'jose';

const keys = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));

const authProvider: AuthProvider = {
  async validateToken(token) {
    try {
      const { payload } = await jwtVerify(token, keys, { issuer: 'https://auth.example.com', audience: 'my-api' });
      return { valid: true, userId: String(payload.sub), metadata: payload };
    } catch {
      return { valid: false, error: 'Invalid token' };
    }
  },
};

Configuration Options ​

Auth Configuration ​

typescript
interface AuthConfig {
  // Enable authentication
  enabled: boolean;
  
  // Auth provider instance. Required when `enabled` is true: createAgentServer throws without one,
  // so a missing provider cannot leave the server open. To name the user some other way, set
  // `enabled: false` and use `resolveUserId` (or `resolveContext`).
  provider?: AuthProvider;
  
  // Header name for token (default: 'Authorization')
  headerName?: string;
  
  // Token prefix to strip (default: 'Bearer ')
  tokenPrefix?: string;
  
  // Routes that don't require authentication
  excludeRoutes?: string[];
  
  // Custom userId resolver (when not using token auth)
  resolveUserId?: (ctx: { headers?: Record<string, string | string[] | undefined>; query: Record<string, string>; body?: unknown; context?: RequestContext }) => Promise<string | undefined>;
}

Exclude Routes ​

typescript
auth: {
  enabled: true,
  provider: authProvider,
  excludeRoutes: [
    '/docs',           // Exact match
    '/docs/*',         // Wildcard - matches /docs/openapi.json
    '/agents',         // Public agent list
    '/health',         // Health check
  ],
}

Client Usage ​

Token Auth ​

bash
# Using cURL
curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3000/api/agents/conversations

# Using fetch
fetch('/api/agents/conversations', {
  headers: {
    'Authorization': 'Bearer your-api-key',
  },
});

JWT Auth ​

typescript
// Login to get JWT
const { token } = await login(username, password);

// Use token in requests
const response = await fetch('/api/agents/conversations', {
  headers: {
    'Authorization': `Bearer ${token}`,
  },
});

Custom Auth Provider ​

Create your own authentication provider:

typescript
interface AuthProvider {
  validateToken(token: string): Promise<AuthResult>;
  generateToken?(userId: string, metadata?: Record<string, unknown>): Promise<string>;
  revokeToken?(token: string): Promise<void>;
}

interface AuthResult {
  valid: boolean;
  userId?: string;
  metadata?: Record<string, unknown>;
  error?: string;
}

class MyAuthProvider implements AuthProvider {
  async validateToken(token: string): Promise<AuthResult> {
    try {
      // Validate with your auth service
      const user = await myAuthService.validateToken(token);
      
      if (user) {
        return {
          valid: true,
          userId: user.id,
        };
      }
      
      return {
        valid: false,
        error: 'Invalid token',
      };
    } catch (error) {
      return {
        valid: false,
        error: 'Authentication failed',
      };
    }
  }
}

const authProvider = new MyAuthProvider();

User ID Resolution ​

From Token ​

The userId is automatically extracted from token validation:

typescript
const authProvider = createTokenAuthProvider({
  tokens: {
    'api-key-1': 'user-123',  // 'user-123' becomes ctx.user.id
  },
});

From a header, the query or the body ​

For unauthenticated scenarios, use resolveUserId:

typescript
auth: {
  enabled: false,  // No token auth
  resolveUserId: async (ctx) => {
    // From a header (set by your gateway)
    return ctx.headers?.['x-user-id'] as string | undefined;

    // Or from the query: return ctx.query?.userId as string | undefined;
  },
}

Listing needs a user ​

GET /conversations and GET /tasks return one user's records, so a request that names no user is refused with 401 identity_required - unless the server sets allowAnonymousList: true (a single-user tool or demo). Token auth, resolveContext with a user, resolveUserId or - without any of those - a ?userId= query parameter all name a user. They are tried in that order and the first one wins: with token auth, a ?userId= or a userId in a create body does not change whose conversations are listed or created; it is only taken when nothing else says who is asking. See Request Context.

Several users ​

typescript
const agentServer = createAgentServer({
  basePath: '/api/agents',
  storage,
  auth: {
    enabled: true,
    provider: authProvider,
  },
});

// A conversation belongs to the user who created it:
// user A lists and opens only A's conversations, user B only B's (403 on someone else's)

The built-in storage providers hold every user's data in one namespace; they separate users, not tenants. For tenants, run a server per tenant (own database or tablePrefix), or scope in your own storage provider using the request context (Request Context).

Security Best Practices ​

  1. Use HTTPS - always, in production. Send tokens in the Authorization header, never in a URL.
  2. Let the server decide who is asking. Prefer resolveContext or token auth over a userId the client names: with either, a client-named userId is ignored.
  3. Short-lived tokens, rotated secrets. Use short expirations for JWTs (set requireExp: true so a token without exp is refused) and rotate secrets; the built-in JWT provider holds one static key and cannot follow a JWKS.
  4. Validate issuer and audience - for JWTs, set issuer and audience.
  5. Hide internal errors - maskServerErrors: true whenever the audience is not the operator.
  6. Decide who may approve and who may raise a budget - hitl.authorizeResolve and authorizeBudget (Approvals, Budgets).
  7. Rate limit - the server implements no limit of its own; use the rateLimit hook (Embedding & limits).
  8. Restrict CORS to exact origins; "*" is for public, token-less servers only.
  9. Validate task callback URLs - the server POSTs to whatever callbackUrl a task names (Task System).
  10. Protect or disable Swagger in production (Swagger UI).
  11. Log authentication failures - monitor them.

Troubleshooting ​

SymptomCause and fix
401 AUTHENTICATION_ERROR "Authentication required"No token in the header. The header is Authorization and the value Bearer <token> unless you set auth.headerName / auth.tokenPrefix. A custom header such as X-API-Key needs headerName: 'X-API-Key'; a value without the prefix is used as it is.
401 "Invalid signature" / "Unsupported algorithm"The token was signed with another key or algorithm than the provider is configured with. The algorithm is the configured one (default HS256), never the token's.
401 "Token expired" / "Token not yet valid"exp / nbf; allow for clock drift with clockToleranceSec.
401 "No user ID in token"The token has no non-empty sub, userId or user_id; set extractUserId.
Swagger UI is behind the loginAdd /docs and /docs/* to auth.excludeRoutes (patterns are matched against the route path relative to basePath; * matches anything).
createAgentServer throws about auth.providerauth.enabled: true needs a provider. To name the user another way, set enabled: false and use resolveUserId or resolveContext.
A token made by generateToken stops working after a restartThey are kept in memory only. Issue and store tokens yourself and validate them with validateFn.

Error Responses ​

The body is always { "error": { "code", "message" } } (see Error Handling):

json
// 401 Unauthorized
{ "error": { "code": "AUTHENTICATION_ERROR", "message": "Authentication required" } }

// 401 Unauthorized (invalid token)
{ "error": { "code": "AUTHENTICATION_ERROR", "message": "Invalid token" } }

// 401 Unauthorized (a list that names no user)
{ "error": { "code": "identity_required", "message": "Listing needs to know who is asking: sign in. ..." } }

// 403 Forbidden (signed in, but it is someone else's)
{ "error": { "code": "FORBIDDEN", "message": "You do not have access to this conversation" } }

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub