Skip to main content
Aegra supports flexible authentication through configurable auth handlers. You write a Python function that verifies credentials and returns user data — Aegra handles the rest.

Quick setup

1

Create an auth file

Create a file (e.g., my_auth.py) in your project root:
2

Add auth to aegra.json

3

Start the server

All API endpoints now require authentication.

The authenticate handler

The @auth.authenticate decorator registers your authentication function. It receives the request headers and must return a dictionary with user data.

Required fields

Optional fields

Any additional fields you return (like role, team_id, etc.) are preserved and accessible via ctx.user in authorization handlers and via the User model in custom routes.

Denying access

Raise any exception to deny authentication:

Authorization handlers

Authorization handlers give you fine-grained access control for specific resources and actions.

Handler types

Handlers can:
  1. Allow — Return None or True (default behavior)
  2. Deny — Return False (returns 403 Forbidden)
  3. Filter — Return a dictionary with filters to apply to queries
  4. Modify — Modify the value dict (e.g., inject metadata)

Resolution priority

Handlers are matched from most specific to least specific:
  1. @auth.on.threads.create — Resource + action
  2. @auth.on.threads — Resource only
  3. @auth.on.*.create — Action only
  4. @auth.on — Global fallback

Examples

Restrict deletion to admins:
Inject metadata on thread creation:
Filter threads by team:

Handler context

The ctx parameter provides:
  • ctx.user — Authenticated user object (with all fields you returned from authenticate)
  • ctx.resource — Resource name ("threads", "assistants", etc.)
  • ctx.action — Action name ("create", "read", "update", "delete", "search")
  • ctx.permissions — User permissions list

Return values

Which routes a handler covers

Every Agent Protocol route dispatches to your handlers. Sub-resources authorize as their parent, so one rule covers a whole family of endpoints: There is no runs resource: the Agent Protocol authorizes run operations under the thread that owns them, and the SDK’s @auth.on has no runs attribute. Stateless runs create an ephemeral thread, so they authorize as threads.create_run. Omitting the thread_id does not bypass your thread rules. A filter dict returned from a threads handler is applied to thread queries, including by-id reads and deletes, not only to search. Your handler runs once per event. A request that legitimately touches several resources still checks each one: creating a cron authorizes crons.create, then assistants.read, then the thread, so you can deny at any layer. Creating a run likewise authorizes threads.create_run, then assistants.read for the assistant it uses. Handlers may also inject data by mutating value in place. Setting value["metadata"]["team_id"] in an @auth.on.threads.create handler stores that metadata on the new thread.
Route-to-handler mapping lives in core/auth_registry.py. A route that is not registered there fails the test suite, so a new endpoint cannot silently skip your handlers.

Accessing user data in your graph

When auth is enabled, Aegra automatically injects the authenticated user’s data into config["configurable"]["langgraph_auth_user"] before graph execution. This happens server-side, so the client cannot tamper with it. This works with all graph types: custom StateGraph graphs, create_react_agent, create_agent, or any compiled graph.

From tools

From graph nodes

Graph nodes can accept a config: RunnableConfig parameter. Aegra injects the authenticated user into config["configurable"]["langgraph_auth_user"] before the graph executes:
The Runtime object (from get_runtime()) does not include config. To access config from nodes, add a config: RunnableConfig parameter to your node function. In tools, use InjectedToolArg or ToolRuntime instead.

From factory graphs

Factory graphs receive a ServerRuntime object that includes runtime.user with the authenticated user’s data. This is available at factory time (when deciding graph structure) before the graph executes:
The runtime.user object is the full User model with all fields from your @auth.authenticate handler. Standard fields (identity, display_name, permissions) and custom fields (role, team_id, etc.) are all accessible as attributes or via dict-style access:
Factory graphs get user data in two places:
  • Factory time (runtime.user on ServerRuntime): for structural decisions like which tools to include, which nodes to add, or which model to use.
  • Execution time (config["configurable"]["langgraph_auth_user"]): available inside nodes and tools during the actual graph run (same as static graphs).

Available fields

The langgraph_auth_user dict contains everything your @auth.authenticate handler returns, including any custom fields:
Convenience shortcuts are also available directly on config["configurable"]:
  • config["configurable"]["user_id"] — the user’s identity
  • config["configurable"]["user_display_name"] — the user’s display name

Provider examples

Auth on custom routes

Custom routes can use authentication via the require_auth dependency:
To require auth on all custom routes by default, set enable_custom_route_auth in your config:
See the custom routes guide for more.

No-auth mode

If no auth is configured, Aegra runs in no-auth mode:
  • All requests are allowed
  • User is set to anonymous
  • Authorization handlers are not called
This is the default for local development and testing.

Configuration options

Non-interruptive design

Authorization handlers are additive by default:
  • If no auth is configured, requests are allowed
  • If no handlers are defined, requests are allowed
  • Handlers only restrict access when they explicitly deny
This ensures Aegra works out of the box without requiring any auth setup.