Custom channel auth
Verify Clerk callers at your eve agent's channel boundary — accept web sessions, API keys, and machine tokens, and map each to an eve principal your tools and instructions can read.
bunx --bun shadcn@latest add clerk/eve-agents/authEverything below builds the channel AuthFn from scratch: session verification first, then multi-token support and attribute mapping.
An eve channel authorizes inbound requests through a list of AuthFn callbacks. Each entry receives the request, returns an eve principal to accept, or returns null to skip to the next entry.
import type { AuthFn } from 'eve/channels/auth'
const myAuth: AuthFn<Request> = async (request) => {
// Return a SessionAuthContext to accept, or `null` to skip.
}Verify a Clerk session
Hand the Request to Clerk's authenticateRequest() and map the verified state to an eve principal.
import { createClerkClient } from '@clerk/backend'
import type { AuthFn } from 'eve/channels/auth'
import { eveChannel } from 'eve/channels/eve'
const clerk = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
})
const clerkAuth: AuthFn<Request> = async (request) => {
const state = await clerk
.authenticateRequest(request, { acceptsToken: 'session_token' })
.catch(() => null)
if (!state?.isAuthenticated) return null
const auth = state.toAuth()
return {
authenticator: 'clerk',
principalType: 'user',
principalId: auth.userId,
subject: auth.userId,
attributes: { tokenType: auth.tokenType },
}
}
export default eveChannel({
auth: [clerkAuth],
})The returned object is what every downstream piece (instructions, tools, subagents) reads as ctx.session.auth.current.
Reject vs. fall through
Returning null skips this authenticator and passes the request to the next entry in the channel's auth chain. If no entry accepts, the request is rejected. Throw UnauthenticatedError instead to short-circuit the chain with a 401 — useful when Clerk is a required authentication strategy.
import { UnauthenticatedError } from 'eve/channels/auth'
if (!state?.isAuthenticated) {
throw new UnauthenticatedError({
code: 'authentication_required',
message: `Clerk auth failed (${state?.reason ?? 'unknown'})`,
})
}Accept multiple token types
Pass an array to acceptsToken to verify more than one Clerk token type in the same call. Switch on auth.tokenType to branch on what authenticated.
const clerkAuth: AuthFn<Request> = async (request) => {
const state = await clerk
.authenticateRequest(request, {
acceptsToken: ['session_token', 'api_key', 'm2m_token', 'oauth_token'],
machineSecretKey: process.env.CLERK_MACHINE_SECRET_KEY,
})
.catch(() => null)
if (!state?.isAuthenticated) return null
const auth = state.toAuth()
if (auth.tokenType === 'session_token') {
return {
authenticator: 'clerk',
principalType: 'user',
principalId: auth.userId,
subject: auth.userId,
attributes: { tokenType: auth.tokenType },
}
}
// API key, M2M, OAuth — Clerk groups these as machine principals.
return {
authenticator: 'clerk',
principalType: 'machine',
principalId: auth.subject,
subject: auth.subject,
attributes: { tokenType: auth.tokenType },
}
}machineSecretKey is required when accepting m2m_token. Clerk uses it to verify the inbound token. See API keys & M2M for setup.
Add attributes
Attributes is a free-form object exposed as shared auth context in instructions, tools, and subagents. Customize it by pulling fields off the Clerk auth object.
Session token:
const attributes: Record<string, string | readonly string[]> = {
tokenType: auth.tokenType,
}
if (auth.orgId) attributes.orgId = auth.orgId
if (auth.orgRole) attributes.role = auth.orgRole
if (auth.orgPermissions?.length) attributes.permissions = auth.orgPermissionsYou can pull anything off auth.sessionClaims to enrich attributes too — useful for custom claims you've added to the session token.
const name = auth.sessionClaims?.name
if (typeof name === 'string') attributes.name = nameMachine variants all share scopes, with additional fields specific to each token type:
const attributes: Record<string, string | readonly string[]> = {
tokenType: auth.tokenType,
}
if (auth.scopes?.length) attributes.scopes = auth.scopes
if (auth.tokenType === 'api_key') {
// API keys are scoped to either a user or an org, never both.
if (auth.userId) attributes.userId = auth.userId
if (auth.orgId) attributes.orgId = auth.orgId
}
if (auth.tokenType === 'oauth_token') {
attributes.userId = auth.userId
attributes.clientId = auth.clientId
}Summary by token type:
See Enrich instructions and Authorize tool calls for how to read these downstream.
Compose with other authenticators
Add other entries to the auth chain. Each runs in order; the first non-null wins.
import { vercelOidc } from 'eve/channels/auth'
export default eveChannel({
auth: [clerkAuth, vercelOidc()],
})Related guides
- API keys & M2M — API key auth and agent-to-agent M2M.
- Enrich instructions — read the principal inside dynamic instructions.
- Authorize tool calls — gate tool invocations against permissions on the principal.
Feedback
Last updated on