Protecting Agents and Routes — Agentuity Documentation

Protecting Agents and Routes

Session auth, API keys, and bearer tokens for agents and routes

Control who can access your agents and routes. Agentuity Auth supports sessions for browser users, API keys for programmatic access, and bearer tokens for service-to-service calls. Built on BetterAuth, it works with any PostgreSQL database.

Quick Start

Install the auth and drizzle packages:

bun add @agentuity/auth @agentuity/drizzle

Create an auth instance, mount the auth routes, and protect your API with session middleware:

typescriptsrc/api/index.ts
import { createRouter } from '@agentuity/runtime';
import { createAuth, mountAuthRoutes, createSessionMiddleware } from '@agentuity/auth';
 
const auth = createAuth({
  connectionString: process.env.DATABASE_URL,
});
 
const router = createRouter();
 
// Expose sign-in, sign-up, session, and token endpoints
router.on(['GET', 'POST'], '/api/auth/*', mountAuthRoutes(auth));
 
// Require a valid session for all other API routes
router.use('/api/*', createSessionMiddleware(auth));
 
export default router;

Auth Methods

MethodWhen to UseHow It Works
SessionBrowser users with a login flowCookie-based, managed by BetterAuth
API KeyServer-to-server or CLI toolsHeader: x-agentuity-auth-api-key or Authorization: ApiKey <key>
Bearer TokenJWT-based service authHeader: Authorization: Bearer <token>

You can check which method authenticated a request with ctx.auth.authMethod, which returns 'session', 'api-key', or 'bearer'.

Auth in Different Contexts

Default Plugins

Agentuity Auth includes four plugins automatically:

PluginPurpose
organizationMulti-tenancy with teams, roles, and invitations
jwtJWT token generation with a JWKS endpoint
bearerBearer token auth via the Authorization header
apiKeyAPI key authentication for programmatic access

Database Setup

Agentuity Auth needs a PostgreSQL database to store users, sessions, and keys.

Connection string (simplest). Provide a URL and Agentuity creates the Bun SQL connection and Drizzle adapter internally:

import { createAuth } from '@agentuity/auth';
 
const auth = createAuth({
  connectionString: process.env.DATABASE_URL,
});

Bring your own adapter. If you already have a Drizzle setup, pass it directly:

import { createAuth } from '@agentuity/auth';
import { drizzleAdapter } from '@agentuity/drizzle';
import { drizzle } from 'drizzle-orm/bun-sql';
import * as authSchema from '@agentuity/auth/schema';
import * as appSchema from './schema';
 
const schema = { ...authSchema, ...appSchema };
const db = drizzle(process.env.DATABASE_URL!, { schema });
 
const auth = createAuth({
  database: drizzleAdapter(db, { provider: 'pg', schema: authSchema }),
});

Environment Variables

VariableDescriptionDefault
DATABASE_URLPostgreSQL connection stringRequired
AGENTUITY_AUTH_SECRETSecret for signing tokensFalls back to BETTER_AUTH_SECRET
AGENTUITY_BASE_URLBase URL for auth callbacksFalls back to BETTER_AUTH_URL

Trusted origins are resolved automatically from AGENTUITY_CLOUD_DOMAINS (set by the platform on deploy) and AUTH_TRUSTED_DOMAINS (comma-separated, developer-set).

OAuth/OIDC Integration

When your app needs to authenticate users through an external OIDC provider (Google, GitHub, Okta, Auth0, or any OIDC-compliant service), use the OAuth flow utilities from @agentuity/core/oauth. These implement the standard authorization code flow with token management and storage.

Environment Variables

Set OAUTH_ISSUER and all endpoint URLs are derived automatically. Override individual endpoints when the provider uses non-standard paths.

VariableDescriptionDefault
OAUTH_CLIENT_IDClient ID from your OIDC providerRequired
OAUTH_CLIENT_SECRETClient secret (required for token exchange)Required
OAUTH_ISSUERBase URL; derives all endpoint URLs--
OAUTH_AUTHORIZE_URLOverride authorize endpoint{issuer}/authorize
OAUTH_TOKEN_URLOverride token endpoint{issuer}/oauth/token
OAUTH_USERINFO_URLOverride userinfo endpoint{issuer}/userinfo
OAUTH_REVOKE_URLOverride revoke endpoint{issuer}/revoke
OAUTH_END_SESSION_URLOverride end-session endpoint{issuer}/end_session
OAUTH_SCOPESSpace-separated scopesopenid profile email

Authorization Code Flow

A typical login flow: redirect the user, exchange the callback code for tokens, then fetch the user profile.

typescriptsrc/api/index.ts
import { createRouter } from '@agentuity/runtime';
import {
  buildAuthorizeUrl,
  exchangeToken,
  fetchUserInfo,
  logout,
} from '@agentuity/core/oauth';
 
const api = createRouter();
 
// Step 1: Redirect user to the OIDC provider
api.get('/oauth/login', (c) => {
  const redirectUri = `${new URL(c.req.url).origin}/api/oauth/callback`;
  const loginUrl = buildAuthorizeUrl(redirectUri, { prompt: 'consent' });
  return c.redirect(loginUrl);
});
 
// Step 2: Handle the callback, exchange code for tokens
api.get('/oauth/callback', async (c) => {
  const code = c.req.query('code');
  if (!code) return c.json({ error: 'Missing authorization code' }, 400);
 
  const redirectUri = `${new URL(c.req.url).origin}/api/oauth/callback`;
  const token = await exchangeToken(code, redirectUri); 
  const user = await fetchUserInfo(token.access_token); 
 
  c.var.logger.info('user authenticated', { sub: user.sub, email: user.email });
  // Store token and set session cookie (see Token Storage below)
  return c.redirect('/');
});
 
// Step 3: Logout, revoke token
api.get('/oauth/logout', async (c) => {
  // Retrieve the stored token (from cookie, KV, or session)
  const refreshToken = c.req.header('x-refresh-token');
  if (refreshToken) {
    await logout(refreshToken); 
  }
  return c.redirect('/');
});
 
export default api;

Every function reads from OAUTH_* environment variables by default. Pass an OAuthFlowConfig object as the last argument to override any setting per-call.

Token Storage

KeyValueTokenStorage persists tokens in ctx.kv and auto-refreshes expired access tokens on retrieval.

typescriptsrc/api/index.ts
import { KeyValueTokenStorage, isTokenExpired } from '@agentuity/core/oauth';
 
// Create once (pass ctx.kv from the request context)
const tokenStore = new KeyValueTokenStorage(ctx.kv, {
  namespace: 'oauth-tokens', // optional, this is the default
  prefix: 'myapp:', // optional key prefix for multi-tenant scoping
  config: { issuer: process.env.OAUTH_ISSUER }, // enables auto-refresh
});
 
// Store tokens after initial exchange
await tokenStore.set('user:123', tokenResponse);
 
// Retrieve: auto-refreshes if expired and a refresh_token exists
const token = await tokenStore.get('user:123');
 
if (token && isTokenExpired(token)) {
  // Auto-refresh failed (no refresh_token, or provider rejected it)
  // Redirect user to login again
}

Token Refresh

Auto-refresh happens transparently when you call tokenStore.get(). If you need to refresh manually (outside of storage), use refreshToken directly:

import { refreshToken } from '@agentuity/core/oauth';
 
const newToken = await refreshToken(previousToken.refresh_token);
// newToken contains a fresh access_token (and possibly a new refresh_token)

Logout

Full logout involves two steps: revoke the token server-side, then remove it from storage.

// Revoke server-side and remove from storage in one call
const removed = await tokenStore.invalidate('user:123');
// `removed` is the token that was stored, or null if none existed

invalidate() revokes the refresh token (or access token as fallback) via the provider's revocation endpoint (RFC 7009), then deletes the entry from KV. Revocation is best-effort: the token is removed from storage even if the revocation request fails.

For standalone revocation without storage, use logout() directly:

import { logout } from '@agentuity/core/oauth';
 
await logout(refreshToken); // Calls the revocation endpoint

For CLI-based OAuth app management (creating clients, rotating secrets, viewing activity), see OAuth Commands.

Next Steps