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.

Who can use your server

Your authenticate handler is the only gate. For each request it does one of two things: Aegra does not look at is_authenticated to decide. It is information for your own code: authorization handlers, graphs and custom routes. A user returned with "is_authenticated": False still gets in. Pick one of the two setups below.

Signed-in users only

Raise for every request you don’t trust. Most deployments want this.
Don’t return a guest or anonymous user to mean “no”. Returning any user lets the request in. To reject a request, raise.

Signed-in users and guests

Return a guest user when there is no token, marked with "is_authenticated": False. Then decide in an authorization handler what guests may do.
A guest can now list and read, but can’t create threads, start runs or change anything. Keep in mind:
  • All guests share the guest identity, so they see each other’s threads and store items. To keep guests apart, give each one its own identity, for example from a session ID your frontend sends.
  • @auth.on is the fallback handler. A more specific handler such as @auth.on.threads.create runs instead of it, so repeat the guest check there.
  • A guest that can start runs spends your model budget. Allow that only on purpose.

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, with is_authenticated set to False
  • Authorization handlers are not called
This is the default for local development and testing. Every caller shares the anonymous user and its data, so don’t expose a server without auth to the internet.

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.