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:
from langgraph_sdk import Auth

auth = Auth()

@auth.authenticate
async def authenticate(headers: dict) -> dict:
    token = headers.get("Authorization", "").replace("Bearer ", "")
    if not token:
        raise Exception("Authentication required")

    # Your verification logic here (JWT decode, OAuth check, etc.)
    return {
        "identity": "user123",
        "display_name": "Jane Doe",
        "permissions": ["read", "write"],
        "is_authenticated": True,
    }
2

Add auth to aegra.json

{
  "graphs": {
    "agent": "./graphs/agent/graph.py:graph"
  },
  "auth": {
    "path": "./my_auth.py:auth"
  }
}
3

Start the server

aegra dev
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

FieldTypeDescription
identitystringUnique user identifier

Optional fields

FieldTypeDefaultDescription
display_namestringSame as identityDisplay name
permissionslist[str][]Permission strings
is_authenticatedboolTrueAuthentication status
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:
@auth.authenticate
async def authenticate(headers: dict) -> dict:
    token = headers.get("Authorization", "").replace("Bearer ", "")
    if not token:
        raise Exception("Authentication required")
    if not is_valid_token(token):
        raise Exception("Invalid token")
    return user_data

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:
@auth.on.assistants.delete
async def restrict_deletion(ctx, value):
    if ctx.user.role != "admin":
        return False
    return None
Inject metadata on thread creation:
@auth.on.threads.create
async def inject_team_id(ctx, value):
    if "metadata" not in value:
        value["metadata"] = {}
    value["metadata"]["team_id"] = ctx.user.team_id
    return value
Filter threads by team:
@auth.on.threads.search
async def filter_by_team(ctx, value):
    return {"metadata": {"team_id": ctx.user.team_id}}

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

ReturnBehavior
None or TrueAllow the request
FalseDeny with 403 Forbidden
dictAllow with filters (e.g., {"metadata": {"team_id": "123"}})
Modified valueAllow with modified request data

Provider examples

import os
from langgraph_sdk import Auth
import jwt

auth = Auth()

JWT_SECRET = os.environ.get("JWT_SECRET")
if not JWT_SECRET:
    raise RuntimeError("JWT_SECRET environment variable is required")

@auth.authenticate
async def authenticate(headers: dict) -> dict:
    token = headers.get("Authorization", "").replace("Bearer ", "")
    if not token:
        raise Exception("Missing Authorization header")

    payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
    return {
        "identity": payload["sub"],
        "display_name": payload.get("name", ""),
        "permissions": payload.get("permissions", []),
        "role": payload.get("role", "user"),
    }

Auth on custom routes

Custom routes can use authentication via the require_auth dependency:
from fastapi import Depends
from aegra_api.core.auth_deps import require_auth
from aegra_api.models.auth import User

@app.get("/custom/whoami")
async def whoami(user: User = Depends(require_auth)):
    return {
        "identity": user.identity,
        "display_name": user.display_name,
        "permissions": user.permissions,
    }
To require auth on all custom routes by default, set enable_custom_route_auth in your config:
{
  "http": {
    "app": "./custom_routes.py:app",
    "enable_custom_route_auth": true
  }
}
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

OptionTypeDefaultDescription
auth.pathstringImport path to your auth handler (./file.py:variable)
auth.disable_studio_authboolfalseDisable auth for LangGraph Studio connections

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.