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
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:- Allow — Return
NoneorTrue(default behavior) - Deny — Return
False(returns 403 Forbidden) - Filter — Return a dictionary with filters to apply to queries
- Modify — Modify the
valuedict (e.g., inject metadata)
Resolution priority
Handlers are matched from most specific to least specific:@auth.on.threads.create— Resource + action@auth.on.threads— Resource only@auth.on.*.create— Action only@auth.on— Global fallback
Examples
Restrict deletion to admins:Handler context
Thectx parameter provides:
ctx.user— Authenticated user object (with all fields you returned fromauthenticate)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 intoconfig["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
- InjectedToolArg (recommended)
- ToolRuntime
InjectedToolArg tells the LLM to ignore this parameter (it won’t try to fill it).
Aegra injects the config automatically at runtime.From graph nodes
Graph nodes can accept aconfig: 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 aServerRuntime
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:
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.useronServerRuntime): 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
Thelanggraph_auth_user dict contains everything your @auth.authenticate
handler returns, including any custom fields:
config["configurable"]:
config["configurable"]["user_id"]— the user’s identityconfig["configurable"]["user_display_name"]— the user’s display name
Provider examples
- JWT
- OAuth
- Firebase
Auth on custom routes
Custom routes can use authentication via therequire_auth dependency:
enable_custom_route_auth in your config:
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
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