How to Secure Custom API Routes in Agent-Native Using getSession and Access Control
Secure custom API routes in Agent-Native by retrieving the session with getSession(event), validating authentication credentials, and enforcing role-based or resource-level permissions before executing sensitive business logic.
Agent-Native, the open-source framework maintained by BuilderIO, builds server-side endpoints on top of the Nitro framework. Custom API routes reside in templates/*/server/routes/ and receive requests as H3 events, requiring explicit authentication checks using the core session management utilities found in packages/core/src/server/auth.ts.
Retrieving the Current Session
Every protected route must extract the caller's identity using the getSession utility. This function inspects incoming request headers for cookies, Bearer tokens, or MCP OAuth credentials and returns a standardized session object containing the user's email, id, role, and custom claims.
The implementation resides in the core package at packages/core/src/server/auth.ts. Import it directly into your route handler:
import { getSession } from "@agent-native/core/server";
import { createError } from "h3";
export default async (event) => {
const session = await getSession(event).catch(() => null);
if (!session) {
throw createError({ statusCode: 401, message: "Unauthenticated" });
}
return { user: session.email };
};
Short-circuit unauthenticated calls immediately after the session check to prevent unauthorized access to database queries or external API calls.
Enforcing Role-Based and Resource-Level Access Control
Once authenticated, apply fine-grained permissions by inspecting the session payload. Common patterns include verifying email domains, checking role arrays, or confirming resource ownership.
In templates/plan/server/plan-asset-route.ts, the implementation demonstrates owner verification:
// templates/plan/server/plan-asset-route.ts
import { getSession } from "@agent-native/core/server";
import { createError } from "h3";
import { db, plans, eq } from "@/db";
export default async (event) => {
const session = await getSession(event).catch(() => null);
if (!session) {
throw createError({ statusCode: 401, message: "Unauthenticated" });
}
const planId = getQuery(event).planId;
const plan = await db.select().from(plans).where(eq(plans.id, planId));
// Resource ownership check
if (plan.ownerId !== session.id) {
throw createError({ statusCode: 403, message: "Forbidden" });
}
return await getPlanAsset(planId);
};
For multi-tenant scenarios, scope queries using the session's workspace id to ensure users only access data within their organization.
Propagating Security Context with runWithRequestContext
Privileged operations often trigger downstream actions that require the same authentication context. Wrap your handler logic inside runWithRequestContext to propagate the session to all @agent-native/core/* helpers, including runAction and database queries.
This pattern appears in administrative endpoints that invoke sensitive actions like seedKitchenSink:
import { getSession, runWithRequestContext } from "@agent-native/core/server";
import { createError } from "h3";
import { seedKitchenSink } from "@/actions/seed-kitchen-sink";
export default async (event) => {
const session = await getSession(event).catch(() => null);
if (!session) throw createError({ statusCode: 401, message: "Unauthenticated" });
// Role-based gate
if (!session.roles?.includes("admin")) {
throw createError({ statusCode: 403, message: "Admin role required" });
}
// Context propagates to all child actions
const result = await runWithRequestContext(event, async () => {
return await seedKitchenSink({ userId: session.id });
});
return result;
};
Creating Reusable Authorization Helpers
Maintain clean route handlers by extracting permission logic into reusable utility functions. These helpers centralize your access control rules and improve testability.
// src/server/utils/auth-helpers.ts
import { createError } from "h3";
export function requireAdmin(session: Session) {
if (!session.roles?.includes("admin")) {
throw createError({ statusCode: 403, message: "Admin role required" });
}
}
export function requireOwner(session: Session, ownerId: string) {
if (session.id !== ownerId) {
throw createError({ statusCode: 403, message: "You do not own this resource" });
}
}
Usage inside a route remains concise:
import { getSession } from "@agent-native/core/server";
import { requireAdmin } from "@/server/utils/auth-helpers";
export default async (event) => {
const session = await getSession(event).catch(() => null);
if (!session) throw createError({ statusCode: 401, message: "Unauthenticated" });
requireAdmin(session);
// Proceed with privileged logic
return await getSensitiveData();
};
Summary
getSession(event)inpackages/core/src/server/auth.tsextracts user identity from multiple authentication mechanisms (cookies, Bearer tokens, MCP OAuth).- Early rejection of unauthenticated requests using
createErrorprevents unauthorized access to business logic. - Resource-level checks compare
session.idagainst resourceownerIdor verifysession.rolesarrays for administrative functions. runWithRequestContextensures downstream actions inherit the same security context, maintaining consistent permissions across the call stack.- Helper utilities like
requireAdminandrequireOwnerkeep route handlers clean and authorization logic centralized.
Frequently Asked Questions
How does getSession handle different authentication methods?
The getSession function implemented in packages/core/src/server/auth.ts normalizes multiple authentication sources. It inspects cookies for session tokens, Authorization headers for Bearer tokens, and MCP OAuth credentials, returning a consistent session object regardless of the authentication mechanism used by the client.
What is the difference between 401 and 403 errors in this context?
Return 401 Unauthorized when getSession returns null or fails to validate credentials, indicating the request lacks authentication entirely. Return 403 Forbidden when the session exists but the user lacks the required role or does not own the requested resource, indicating insufficient permissions despite valid credentials.
Can I use middleware to protect multiple routes at once?
While the examples show inline checks for clarity, you can extract the authentication pattern into a Nitro middleware function that wraps route handlers. The middleware would call getSession, verify the session exists, and optionally attach the session object to the event context before calling the main handler, though explicit per-route checks remain the recommended pattern for visibility.
How do I test protected routes during development?
Create test users with specific roles in your development environment, then generate valid session tokens or cookies for those users. Pass these credentials in your test requests' headers or cookies when calling the API route. The getSession function will validate them against your development authentication provider just as it does in production.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →