Skip to main content

SDKs & Libraries

LumoAuth ships official, hand-written SDKs for JavaScript/TypeScript, Python, Go, PHP and .NET, plus generated API clients for TypeScript, Python and Go that cover every REST endpoint. Every package below is on its public registry; install it with the command shown.

Which package do I install?​

One obvious install per app type:

Your appInstall
Next.jsnpm install @lumoauth/nextjs (server helpers at @lumoauth/nextjs/server)
React SPAnpm install @lumoauth/react
Vue, Angular, Svelte or other browser appnpm install @lumoauth/client
Expressnpm install @lumoauth/express
Plain Node.js servernpm install @lumoauth/backend
JS/TS AI agent (AAuth)npm install @lumoauth/agent
New agent project from scratchnpx create-lumo-agent my-agent
Pythonpip install lumoauth
FastAPIpip install 'lumoauth[fastapi]'
Gogo get github.com/lumoauth/lumo-auth-go
PHPcomposer require lumoauth/lumoauth — see PHP
Symfonycomposer require lumoauth/symfony-bundle
Laravelcomposer require lumoauth/laravel
.NETdotnet add package LumoAuth — see C# / .NET
ASP.NET Coredotnet add package LumoAuth.AspNetCore
Full API surface (all ~300 endpoints)the generated client for TypeScript, Python or Go (list), also exposed on the server SDKs as the lumo.api (JS) / client.api (Python) escape hatch

@lumoauth/shared (isomorphic core) and @lumoauth/client (framework-agnostic browser client) are the internals the React and Next.js packages build on; install @lumoauth/client directly for a Vue, Angular, or other non-React browser app.

Packages​

PackageStackInstall
@lumoauth/clientBrowser TS/JS — LumoAuth client, OAuth/PKCE flows, RBAC/ABAC/Zanzibarnpm install @lumoauth/client
@lumoauth/backendNode.js servers — LumoAuthBackend, same authorization surface plus the lumo.api escape hatchnpm install @lumoauth/backend
@lumoauth/expressExpress middleware: lumoAuthMiddleware() + requireAuth()npm install @lumoauth/express
@lumoauth/agentNode.js AI agents — LumoAgent, AAuth keys, signed HTTP requests, AAuthClientnpm install @lumoauth/agent
@lumoauth/reactReact SPA — <SignIn />, <UserButton />, useLumoAuth()npm install @lumoauth/react
@lumoauth/nextjsNext.js — re-exports the React SDK; server helpers at @lumoauth/nextjs/servernpm install @lumoauth/nextjs
create-lumo-agentScaffolds a Next.js agent app (template nextjs-agent)npx create-lumo-agent my-agent
lumoauthPython — LumoAuth client, agent identity (AAuth), JIT permissions, delegationpip install lumoauth
lumoauth[aauth]Python — Ed25519 signing for AAuthpip install 'lumoauth[aauth]'
lumoauth[fastapi]FastAPI router + get_current_user dependencypip install 'lumoauth[fastapi]'
lumo-auth-goGo — Client, Agent, AAuth, net/http and chi-compatible web sign-ingo get github.com/lumoauth/lumo-auth-go
lumoauth/lumoauthPHP 8.1+ — LumoAuth client, AAuth signing, PSR-18 transportcomposer require lumoauth/lumoauth
lumoauth/symfony-bundleSymfony 6.4 / 7 — configuration, autowired LumoAuth, access_token handlercomposer require lumoauth/symfony-bundle
lumoauth/laravelLaravel 10–12 — service provider, LumoAuth facade, lumoauth.permission middlewarecomposer require lumoauth/laravel
LumoAuth.NET 8+ — LumoAuthClient, LumoAgent, AAuth signing and verificationdotnet add package LumoAuth
LumoAuth.AspNetCoreASP.NET Core 8+ — AddLumoAuth(configuration), bearer tokens + OpenID Connect sign-in, [RequirePermission], health checkdotnet add package LumoAuth.AspNetCore

All of these are built to the same contract: the same resource namespaces, the same error taxonomy, the same environment variables (LUMOAUTH_URL, LUMOAUTH_ORG_ID, LUMOAUTH_API_KEY, AGENT_CLIENT_ID, AGENT_CLIENT_SECRET) and byte-compatible AAuth request signing, so one .env serves a polyglot stack.

Generated API clients​

A generated client is a raw HTTP client with typed models for every REST endpoint (about 300 operations), produced from the OpenAPI spec. Reach for one when you need an endpoint the hand-written SDK does not wrap, for example in admin automation or SCIM tooling. The server-side SDKs already expose it: lumo.api on @lumoauth/backend and client.api on the Python package.

LanguagePackageInstall
TypeScript@lumoauth/api-clientnpm install @lumoauth/api-client
Pythonlumoauth-api-clientpip install lumoauth-api-client (or pip install 'lumoauth[api]')
Gogithub.com/lumoauth/api-clients/gogo get github.com/lumoauth/api-clients/go

The hand-written SDKs add framework-native middleware, sessions and agent helpers on top of the same REST API. All SDKs work with LumoAuth's multi-organization architecture out of the box.


JavaScript / TypeScript SDK​

@lumoauth/client (browser) and @lumoauth/backend (Node.js servers) — the core clients. Both expose the same authorization surface (auth, permissions, zanzibar, abac, agent); they differ only in the credential model (browser user token vs. server secret) and runtime guards.

npm install @lumoauth/client # browser
npm install @lumoauth/backend # Node server

Initialize the Client​

Browser (public client, PKCE):

import { LumoAuth } from '@lumoauth/client';

const lumo = new LumoAuth({
baseUrl: 'https://app.lumoauth.dev',
orgId: 'acme-corp',
clientId: 'your-client-id',
authStrategy: 'pkce', // default
});

Server (organization API key or machine token — never expose to the browser):

import { LumoAuthBackend } from '@lumoauth/backend';

const lumo = new LumoAuthBackend({
baseUrl: process.env.LUMOAUTH_URL!,
secretKey: process.env.LUMOAUTH_SECRET_KEY!, // lmk_… API key or machine token
orgId: 'acme-corp',
});

For the ~300 admin and platform endpoints beyond this curated surface, use the generated @lumoauth/api-client — exposed on the backend SDK as the lumo.api escape hatch (lumo.api.create(AdminUsersApi)).

PKCE Authentication​

// Generate authorization URL (buildAuthorizationUrl is async)
const { url, codeVerifier, state } = await lumo.auth.buildAuthorizationUrl({
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile email',
});

// Redirect user to `url`, then exchange the code on callback
const tokens = await lumo.auth.exchangeCodeForTokens({
code: authorizationCode,
codeVerifier,
redirectUri: 'https://myapp.com/callback',
});
// tokens.access_token, tokens.id_token, tokens.refresh_token

Permission Checks (RBAC)​

const allowed = await lumo.permissions.check('documents.edit');
// true | false

const bulk = await lumo.permissions.checkBulk([
'documents.edit',
'documents.delete',
'users.invite',
]);
// { results: { 'documents.edit': true, 'documents.delete': false, 'users.invite': true } }

Zanzibar (ReBAC)​

const canView = await lumo.zanzibar.check({
object: 'document:report-q4',
relation: 'viewer',
subject: 'user:jane',
});

// Convenience helpers
const isOwner = await lumo.zanzibar.isOwner('document:report-q4', 'user:jane');
const isEditor = await lumo.zanzibar.isEditor('project:alpha', 'user:bob');

check answers "does this subject have the relation". To ask "who has it at all", expand the relation into its userset tree:

const tree = await lumo.zanzibar.expand({
object: 'document:report-q4',
relation: 'viewer',
});
// { type: 'union', object: 'document:report-q4', relation: 'viewer',
// children: [{ type: 'leaf', subjects: ['user:jane', 'team:eng#member'], ... }] }

Leaf nodes carry subjects (usersets are listed as-is, e.g. team:eng#member); union / intersection nodes carry children of the same shape. The same call exists in the other two SDKs:

tree = lumo.zanzibar.expand("document:report-q4", "viewer")
tree, err := client.Zanzibar.Expand(ctx, "document:report-q4", "viewer")
Expansion needs the oracle privilege

Expanding a relation reveals other subjects, so it requires the authz.check permission or the authz:check scope — there is no self-service variant. A token that can only check its own access gets a 403.

ABAC Policy Evaluation​

const result = await lumo.abac.isAllowed('document', 'edit', 'doc-123');
// true | false

const detailed = await lumo.abac.check({
resourceType: 'document',
action: 'edit',
resourceId: 'doc-123',
context: { ip: '203.0.113.1' },
});
// { allowed: true, reason: '...', matchedPolicies: [...] }

Error Handling​

import {
LumoAuthApiError,
LumoAuthAuthenticationError,
LumoAuthPermissionDeniedError,
} from '@lumoauth/client';
// (also re-exported from @lumoauth/backend; defined in @lumoauth/shared)

try {
await lumo.permissions.check('admin.access');
} catch (err) {
if (err instanceof LumoAuthAuthenticationError) {
// 401 - token expired or invalid
} else if (err instanceof LumoAuthPermissionDeniedError) {
// 403
} else if (err instanceof LumoAuthApiError) {
// Other API error (4xx/5xx)
console.error(err.statusCode, err.code, err.message);
}
}

Every error extends LumoAuthError and carries a stable code; the same taxonomy (LumoAuthNotFoundError, LumoAuthRateLimitError, LumoAuthNetworkError, LumoAuthValidationError, LumoAuthConfigError) is shared by every SDK.


Express.js middleware​

@lumoauth/express — server-side Express integration. Mounts the OAuth login flow as three routes (/auth/login, /auth/callback, /auth/logout) and exposes requireAuth() to gate handlers. It depends on @lumoauth/backend, so one install also gives you LumoAuthBackend for direct permission checks.

npm install @lumoauth/express express express-session
import "dotenv/config";
import express from "express";
import session from "express-session";
import { lumoAuthMiddleware, requireAuth } from "@lumoauth/express";

const app = express();

// Session middleware MUST be mounted before lumoAuthMiddleware. Use a
// production session store (Redis, Postgres, …); the default in-memory
// store loses sessions on restart.
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production" },
}));

app.use(lumoAuthMiddleware({
baseUrl: process.env.LUMO_BASE_URL, // https://app.lumoauth.dev
organization: process.env.LUMO_ORG_ID, // your organization slug
clientId: process.env.LUMO_CLIENT_ID,
clientSecret: process.env.LUMO_CLIENT_SECRET, // omit for public clients
prefix: "/auth", // default: /auth
postLoginRedirect: "/", // where to land after sign-in
postLogoutRedirect: "/", // where to land after sign-out
scope: "openid profile email", // default
}));

app.get("/", (req, res) => {
if (!req.user) return res.redirect("/auth/login");
res.send(`<h1>Hi ${req.user.email}</h1><a href="/auth/logout">Sign out</a>`);
});

// Gate any handler with requireAuth(). 401 if no session;
// 403 if `scopes` are missing.
app.get("/api/me", requireAuth(), (req, res) => res.json(req.user));

app.get("/api/admin", requireAuth({ scopes: ["admin"] }), (req, res) => {
res.json({ ok: true });
});

// Programmatic check via the `authorize` hook.
app.get("/api/billing", requireAuth({
authorize: async (user, req) => {
// call out to RBAC / Zanzibar / your DB here; return true/false.
return user.email?.endsWith("@acme.com") ?? false;
},
}), (req, res) => res.json({ ok: true }));

What lumoAuthMiddleware does​

PathHandler
GET {prefix}/loginBuilds a PKCE-protected authorize URL, stores verifier+state on the session, redirects to LumoAuth. Honours ?return_to=/some/path (same-origin only — open-redirect-safe).
GET {prefix}/callbackVerifies state, exchanges the code via PKCE (+ optional client_secret for confidential clients), fetches /userinfo, persists user+tokens on the session, regenerates the session id (fixation defence), and redirects to return_to.
GET|POST {prefix}/logoutWipes LumoAuth state from the session (leaves any unrelated keys alone) and redirects to postLogoutRedirect. The host app calls req.session.destroy() itself if it wants a hard wipe.
every other requestRestores req.user from the session for downstream handlers.

requireAuth(options)​

OptionEffect
scopes: string[]Returns 403 when any listed scope is missing from the session token.
authorize(user, req)Async hook for programmatic checks (RBAC / Zanzibar / DB lookup). Return false to deny.
unauthorizedStatus: numberOverride the 401/403 status.

Session requirements​

The host app must mount express-session (or any compatible store exposing req.session) before lumoAuthMiddleware. The middleware hard-fails with a 500 + session_middleware_missing error if it isn't mounted — silently passing through would mask the real bug at sign-in time, when the missing session is most disruptive.

LumoAuth state lives under req.session.__lumoAuth so it doesn't collide with anything the app stores on the session itself.


FastAPI router​

lumoauth.fastapi — Python FastAPI integration. lumo_auth_router(...) returns an APIRouter you include_router(...) under a prefix; the get_current_user dependency injects the signed-in user into protected routes.

pip install 'lumoauth[fastapi]'
import os
from fastapi import FastAPI, Depends
from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from lumoauth.fastapi import lumo_auth_router, get_current_user, require_auth, User

app = FastAPI()

# SessionMiddleware MUST come first — lumoauth.fastapi reads from
# request.session and fails fast with a clear message if it's missing.
app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"])

app.include_router(
lumo_auth_router(
base_url=os.environ["LUMO_BASE_URL"],
organization=os.environ["LUMO_ORG_ID"],
client_id=os.environ["LUMO_CLIENT_ID"],
client_secret=os.environ.get("LUMO_CLIENT_SECRET"), # omit for public
callback_path="/auth/callback", # default
scope="openid profile email", # default
post_login_redirect="/",
post_logout_redirect="/",
),
prefix="/auth",
)

@app.get("/")
def home(user: User | None = Depends(get_current_user)):
if user is None:
return RedirectResponse("/auth/login")
return {"hi": user.email}

# Gating dependency. 401 if no user; 403 if any required scope is missing.
@app.get("/api/me")
def me(user: User = Depends(require_auth())):
return user.model_dump()

@app.get("/api/admin")
def admin(user: User = Depends(require_auth(scopes=["admin"]))):
return {"ok": True}

What the router mounts​

lumo_auth_router(prefix="/auth") exposes:

PathMethodHandler
/auth/loginGETPKCE authorize redirect, optional ?return_to=/... (same-origin only).
/auth/callbackGETVerifies state, exchanges code, persists user+tokens on request.session["__lumoauth"], redirects to return_to.
/auth/logoutGET / POSTWipes request.session["__lumoauth"], redirects to post_logout_redirect.

Dependencies​

from lumoauth.fastapi import get_current_user, require_auth

# get_current_user: returns Optional[User] — for routes that work for
# both authenticated and anonymous callers.
@app.get("/dashboard")
def dashboard(user: User | None = Depends(get_current_user)):
if user is None:
return RedirectResponse("/auth/login")
return {"name": user.name}

# require_auth(): factory returning a dependency that 401s when no user.
# Pass `scopes=[...]` to also 403 on missing scopes.
@app.get("/admin")
def admin(user: User = Depends(require_auth(scopes=["admin", "billing"]))):
return {"ok": True}

The User model is a Pydantic v2 BaseModel with extra="allow" — custom claims from /userinfo (org-specific traits, custom scopes) pass through as attributes without losing type safety on the standard ones.


Go SDK​

github.com/lumoauth/lumo-auth-go — Go SDK for net/http and chi.

go get github.com/lumoauth/lumo-auth-go
package main

import (
"fmt"
"net/http"
"os"

"github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
lumoauth "github.com/lumoauth/lumo-auth-go"
)

func main() {
_ = godotenv.Load()

auth, err := lumoauth.NewWebAuth(lumoauth.WebConfig{
BaseURL: os.Getenv("LUMO_BASE_URL"), // https://app.lumoauth.dev
Organization: os.Getenv("LUMO_ORG_ID"), // organization slug
ClientID: os.Getenv("LUMO_CLIENT_ID"),
ClientSecret: os.Getenv("LUMO_CLIENT_SECRET"), // omit for public clients
SessionSecret: os.Getenv("SESSION_SECRET"), // 32+ bytes
CallbackPath: "/auth/callback", // default
Scope: "openid profile email", // default
PostLoginRedirect: "/",
})
if err != nil {
panic(err)
}

r := chi.NewRouter()

// Mount the three auth routes under the prefix matching CallbackPath.
r.Mount("/auth", auth.Router())

// Attach Middleware on routes that should know who the user is.
r.Group(func(r chi.Router) {
r.Use(auth.Middleware)

r.Get("/", func(w http.ResponseWriter, req *http.Request) {
user := lumoauth.UserFromContext(req.Context())
if user == nil {
http.Redirect(w, req, "/auth/login", http.StatusFound)
return
}
fmt.Fprintf(w, "Hi %s", user.Email)
})

// Gate with RequireAuth — 401 if no user.
r.With(auth.RequireAuth).Get("/api/me", func(w http.ResponseWriter, req *http.Request) {
user := lumoauth.UserFromContext(req.Context())
fmt.Fprintf(w, "%s\n", user.Email)
})
})

http.ListenAndServe(":3000", r)
}

API surface​

SymbolPurpose
NewWebAuth(WebConfig) (*WebAuth, error)Validates config, returns the auth handle. Error on missing BaseURL, Organization, ClientID, SessionSecret.
(*WebAuth).Router() http.HandlerA plain http.Handler serving /login, /callback, /logout; mounts into chi, gorilla/mux or net/http alike. Mount at the prefix matching CallbackPath.
(*WebAuth).Middleware(http.Handler) http.HandlerReads the signed-in user from session into context. Add to chains where you call UserFromContext.
(*WebAuth).RequireAuth(http.Handler) http.HandlerGates a handler with 401 when no user is present.
(*WebAuth).RequireAuthRedirect(loginPath)Sends anonymous visitors to sign in instead, preserving where they were going.
(*WebAuth).ClientForRequest(req) *ClientA Client authenticated as the signed-in user, so authorization checks run under their identity.
(*WebAuth).RefreshSession(w, req)Renews an expired access token from the stored refresh token.
UserFromContext(ctx) *UserReturns the signed-in user for this request, or nil.

What WebConfig controls​

FieldDefaultPurpose
BaseURL(required)LumoAuth deployment URL
Organization(required)Organization slug
ClientID(required)OAuth client id
ClientSecret(empty)Required for confidential clients only
SessionSecret(required)Cookie-store key, ≥32 bytes
CallbackPath/auth/callbackMust match a registered redirect URI
Scopeopenid profile emailSpace-separated scopes
PostLoginRedirect/Where to redirect after sign-in
PostLogoutRedirect/Where to redirect after sign-out
HTTPClient10s timeoutOverride for tests / instrumentation
CookieNamelumo_sessionSession cookie name
CookieSecuretrueSecure flag on the session cookie; set lumoauth.Bool(false) only for local development over plain HTTP
SessionMaxAge30 daysSession cookie lifetime

Reverse proxies​

The middleware honours standard reverse-proxy headers (X-Forwarded-Proto, X-Forwarded-Host) when constructing absolute callback URLs. Behind a TLS-terminating proxy your CallbackPath will resolve to the public https://... URL even though the Go process listens on plain HTTP.


React SDK​

@lumoauth/react - Pre-built components and hooks for React 18+.

npm install @lumoauth/react

react and react-dom 18 or newer are peer dependencies. @lumoauth/client is installed automatically and re-exported, so one install is enough.

Provider Setup​

Wrap your app with LumoAuthProvider:

import { LumoAuthProvider } from '@lumoauth/react';

function App() {
return (
<LumoAuthProvider
domain="https://app.lumoauth.dev"
orgId="acme-corp"
clientId="your-client-id"
>
<MyApp />
</LumoAuthProvider>
);
}

Authentication Components​

import { SignIn, SignUp, AuthCallback, UserButton } from '@lumoauth/react';

// Sign-in page
<SignIn afterSignInUrl="/dashboard" />

// Sign-up page
<SignUp afterSignUpUrl="/onboarding" />

// OAuth callback handler
<AuthCallback afterSignInUrl="/dashboard" />

// User menu dropdown (avatar + sign out)
<UserButton showName />

Conditional Rendering​

import { SignedIn, SignedOut, RedirectToSignIn } from '@lumoauth/react';

function Layout() {
return (
<>
<SignedIn>
<Dashboard />
</SignedIn>
<SignedOut>
<RedirectToSignIn />
</SignedOut>
</>
);
}

Authorization with Protect​

import { Protect } from '@lumoauth/react';

// RBAC
<Protect permission="billing.manage" fallback={<p>No access</p>}>
<BillingDashboard />
</Protect>

// Zanzibar
<Protect zanzibar={{ object: 'project:alpha', relation: 'editor' }}>
<ProjectEditor />
</Protect>

// ABAC
<Protect abac={{ resourceType: 'document', action: 'edit', resourceId: 'doc-123' }}>
<DocumentEditor />
</Protect>

Hooks​

import {
useAuth,
useUser,
usePermission,
useZanzibar,
useAbac,
useLumoAuth,
} from '@lumoauth/react';

function Dashboard() {
const { isSignedIn, signOut, getToken } = useAuth();
const user = useUser();
const { allowed: canEdit } = usePermission('documents.edit');
const { allowed: isViewer } = useZanzibar({
object: 'project:alpha',
relation: 'viewer',
});

if (!isSignedIn) return null;

return (
<div>
<p>Welcome, {user.displayName}</p>
{canEdit && <EditButton />}
<button onClick={signOut}>Sign Out</button>
</div>
);
}

Send passwordless magic-link emails and implement email-first login flows using purpose-built hooks.

import { useMagicLink } from '@lumoauth/react';

function MagicLinkForm() {
const [email, setEmail] = useState('');
const { sendMagicLink, isLoading, isSent, error, reset } = useMagicLink();

if (isSent) {
return (
<div>
<p>Check your inbox — we sent a link to <strong>{email}</strong>.</p>
<button onClick={reset}>Use a different email</button>
</div>
);
}

return (
<form onSubmit={(e) => { e.preventDefault(); sendMagicLink(email); }}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending…' : 'Send sign-in link'}
</button>
{error && <p>{error}</p>}
</form>
);
}
ReturnTypeDescription
sendMagicLink(email, redirectUri?) => Promise<void>Send the magic link
isLoadingbooleanTrue while the request is in-flight
isSentbooleanTrue after a successful request
errorstring | nullError message if the request failed
reset() => voidReset back to idle

useEmailFirst()​

Check whether an account exists for a given email before revealing the next step:

import { useEmailFirst } from '@lumoauth/react';

function EmailStep({ onKnownUser, onNewUser }) {
const [email, setEmail] = useState('');
const { checkEmail, isLoading } = useEmailFirst();

async function handleSubmit(e) {
e.preventDefault();
const exists = await checkEmail(email);
exists ? onKnownUser(email) : onNewUser(email);
}

return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit" disabled={isLoading}>
{isLoading ? 'Checking…' : 'Continue'}
</button>
</form>
);
}
ReturnTypeDescription
checkEmail(email) => Promise<boolean>Returns true if account exists
isLoadingbooleanTrue while the check is in-flight
existsboolean | nullResult of last check (null = not yet checked)
reset() => voidReset state back to idle

You can also call the AuthModule directly for non-React environments:

import { AuthModule } from '@lumoauth/client';

const auth = new AuthModule({
baseUrl: 'https://app.lumoauth.dev',
orgId: 'acme-corp',
clientId: 'your-client-id',
});

// Send a magic link
await auth.requestMagicLink({ email: 'user@example.com' });

// Email-first check
const { exists } = await auth.checkEmailExists('user@example.com');

See the Magic Link & Email-First guide for full configuration details.

Theming​

<LumoAuthProvider
domain="https://app.lumoauth.dev"
orgId="acme-corp"
clientId="your-client-id"
>
<SignIn appearance={{ theme: 'dark', className: 'my-signin' }} />
</LumoAuthProvider>

Python SDK​

lumoauth - Python SDK for server-side applications and AI agents.

pip install lumoauth

Python 3.9 to 3.13, one dependency (requests). Extras: lumoauth[aauth] (Ed25519 signing for AAuth), lumoauth[fastapi] (sign-in routes for FastAPI) and lumoauth[api] (the generated client behind client.api).

Backend client​

LumoAuth is the general-purpose client, organised into resource namespaces (auth, permissions, zanzibar, abac, agents, delegation, jit, approvals, mcp). It reads LUMOAUTH_URL, LUMOAUTH_ORG_ID and LUMOAUTH_API_KEY from the environment when constructed without arguments.

from lumoauth import LumoAuth

client = LumoAuth(api_key="lk_…", org_id="acme-corp")

# RBAC: does the caller hold this permission?
if client.permissions.check("document.edit", user_id="user_42"):
...

# ReBAC (Zanzibar): is bob a viewer of this document?
if client.zanzibar.is_viewer("document:readme", "user:bob"):
...

# ABAC: evaluate attribute-based policy for a resource
decision = client.abac.check("document", "read", "doc-123")
if decision["allowed"]:
...

Every HTTP failure raises a subclass of LumoAuthError (LumoAuthAuthenticationError for 401, LumoAuthPermissionDeniedError for 403, LumoAuthRateLimitError with retry_after for 429, …).

Agent Authentication​

from lumoauth import LumoAuthAgent

agent = LumoAuthAgent(
base_url="https://app.lumoauth.dev",
org_id="acme-corp",
client_id="your-agent-client-id",
client_secret="your-agent-client-secret",
)

agent.authenticate(scopes=["read:users", "write:documents"])

Environment variables are also supported (LUMOAUTH_URL, LUMOAUTH_ORG_ID, AGENT_CLIENT_ID, AGENT_CLIENT_SECRET).

The Ask API​

Lightweight preflight checks optimized for LLM tool-calling workflows:

result = agent.ask("read_document", context={"document_id": "doc-123"})
# { 'allowed': True, 'action': 'read_document', 'reason': '...', 'audit_id': '...' }

if agent.is_allowed("delete_user", context={"user_id": "u-456"}):
# proceed with action
pass

AI Access Control​

info = agent.get_agent_info()
# { 'id': ..., 'capabilities': [...], 'budget_policy': {...} }

if agent.has_capability("web_search"):
# agent is authorized for web search
pass

budget = agent.get_budget_status()
if agent.is_budget_exhausted():
print("Daily token budget reached")

Delegation Chains (token exchange)​

Act on behalf of users with token exchange, which swaps the user's consent for a token that records both the agent and the user it acts for:

from lumoauth import DelegationChain

chain = DelegationChain(agent, redirect_uri="https://myapp.com/consent")

# Get user consent
consent_url = chain.get_consent_url("session-1", scopes=["profile", "documents"])
# Redirect user to consent_url...

# After consent callback
chain.handle_consent_callback("session-1", authorization_code)

# Exchange for delegated token
delegated_token = chain.exchange("session-1", scopes=["read:documents"])

# Make requests on behalf of the user
response = chain.request("session-1", "GET", "/api/v1/documents")

MCP Token Exchange​

mcp_token = agent.get_mcp_token("my-mcp-server")
# Use mcp_token to authenticate with the MCP server

Cryptographic AI Access Control (AAuth)​

For agents that need cryptographic identity with HTTP message signing:

pip install 'lumoauth[aauth]'
from lumoauth import AAuthClient

# Generate a keypair
private_pem, jwks = AAuthClient.generate_keypair()

client = AAuthClient(
agent_identifier="https://myagent.example.com",
private_key_pem=private_pem,
base_url="https://app.lumoauth.dev",
org_id="acme-corp",
)

# Sign HTTP requests (RFC 9421)
headers = client.sign_request("POST", "https://api.example.com/data", body=b'{"key":"value"}')

sign_request returns the Signature, Signature-Input and Content-Digest headers for a signed HTTP request; signed_request(...) sends the request for you with a bearer token attached.


OIDC Discovery​

All SDKs work with LumoAuth's standard OIDC discovery endpoint. Any third-party OAuth 2.0 / OIDC library can also integrate by pointing to:

https://app.lumoauth.dev/orgs/{orgId}/api/v1/.well-known/openid-configuration

Framework Quickstarts​

Get started with a complete integration guide for your stack:

PHP​

The hand-written PHP SDK ships as three Composer packages on Packagist, built to the same contract as the JS, Python and Go SDKs: the same namespaces, errors and environment variables, and byte-compatible AAuth signing. PHP 8.1+, PSR-18 transport (Guzzle or any other client is discovered automatically), ext-sodium for AAuth signing.

composer require lumoauth/lumoauth # plain PHP
composer require lumoauth/symfony-bundle # Symfony 6.4 / 7: config + autowired client + access_token handler
composer require lumoauth/laravel # Laravel 10–12: provider, facade, lumoauth.permission middleware
Constructor argumentEnvironment variableDefault
baseUrlLUMOAUTH_URLhttps://app.lumoauth.dev
orgIdLUMOAUTH_ORG_IDrequired for org-scoped calls
apiKeyLUMOAUTH_API_KEYsent as X-API-Key
tokenProvider—a callable returning a bearer token; wins over the API key
use LumoAuth\LumoAuth;

$lumo = new LumoAuth(apiKey: getenv('LUMOAUTH_API_KEY'), orgId: 'acme-corp');

if ($lumo->permissions->check('document.edit', userId: $userId)) { … }
$allowed = $lumo->zanzibar->isViewer('document:readme', 'user:bob');
$decision = $lumo->abac->check('document', 'read', 'doc-123');
$verdict = $lumo->agents->ask('send_email', ['to' => 'cfo@acme.com']);
$result = $lumo->approvals->require(taskId: $t, reason: 'Deploy', onBehalfOf: 'alice@acme.com');
NamespaceExample
permissionscheck('document.edit'), checkAny([...]), checkBulk([...]), listSlugs()
zanzibarisOwner('document:1', 'user:alice'), expand('document:1', 'viewer')
abacisAllowed('document', 'read', 'd1', ['ip' => …]), setUserAttribute(...)
agentsask('send_email', $ctx), me(), register(...), capabilities()
approvalsrequire(taskId: …, reason: …, onBehalfOf: …)->isApproved()
jitcreateTask(...), request($taskId, $authorizationDetails), token($requestId)
mcpgetToken('urn:mcp:finance')
delegationexchange($subjectToken, $actorToken), Delegation::parseActorChain($jwt)
authauthorizationUrl(...), exchangeCode(...), clientCredentials(...), userinfo()

Every failure is a LumoAuth\Error\LumoAuthError with errorCode and, for API errors, statusCode and body: LumoAuthAuthenticationError (401), LumoAuthPermissionDeniedError (403), LumoAuthNotFoundError (404), LumoAuthRateLimitError (429, retryAfter), LumoAuthValidationError, LumoAuthConfigError, LumoAuthNetworkError.

AAuth agents use LumoAuth\AAuth\AAuthClient and LumoAuth\AAuth\Signer:

use LumoAuth\AAuth\AAuthClient;
use LumoAuth\AAuth\Signer;

[$pem, $jwks] = Signer::generateKeypair(); // publish $jwks at /.well-known/jwks.json
$aauth = new AAuthClient($pem, $lumo->http(), orgId: 'acme-corp');
$grant = $aauth->requestAuthorization($resourceToken, 'read write', agentToken: $agentJwt);
$resp = $aauth->signedRequest('GET', 'https://rs.example/docs', $grant['access_token']);

Symfony bundle​

# config/packages/lumoauth.yaml
lumoauth:
url: '%env(LUMOAUTH_URL)%'
org_id: '%env(LUMOAUTH_ORG_ID)%'
api_key: '%env(LUMOAUTH_API_KEY)%'

LumoAuth\LumoAuth is then autowirable:

public function __construct(private LumoAuth $lumo) {}

if (!$this->lumo->permissions->check('document.edit', userId: $user->getUserIdentifier())) {
throw $this->createAccessDeniedException();
}

Authenticate API requests with LumoAuth bearer tokens through the security component's access_token authenticator; the badge carries the userinfo claims so your user provider needs no second call:

security:
firewalls:
api:
pattern: ^/api
stateless: true
access_token:
token_handler: LumoAuth\Symfony\Security\LumoAuthAccessTokenHandler

Laravel package​

composer require lumoauth/laravel
php artisan vendor:publish --tag=lumoauth-config # optional; env vars work without it
LUMOAUTH_URL=https://app.lumoauth.dev
LUMOAUTH_ORG_ID=acme-corp
LUMOAUTH_API_KEY=lk_…

Inject LumoAuth\LumoAuth anywhere the container resolves, or use the LumoAuth facade; the middleware alias lumoauth.permission requires every listed permission for the signed-in user:

Route::put('/documents/{id}', UpdateDocument::class)
->middleware('lumoauth.permission:document.edit');

Beyond the wrapped namespaces​

Anything the namespaces do not wrap is reachable with the same credentials through the transport, $lumo->http()->request('GET', $lumo->http()->orgPath('/documents')); the API reference documents every endpoint.

C# / .NET​

The hand-written .NET SDK ships as two packages on NuGet, built to the same contract as the JS, Python, Go and PHP SDKs: the same namespaces, errors and environment variables, and byte-compatible AAuth signing. .NET 8+ (consumable from .NET 8, 9 and 10 apps), System.Text.Json, async throughout; the only third-party dependency is BouncyCastle.Cryptography for Ed25519.

dotnet add package LumoAuth # any .NET 8+ project
dotnet add package LumoAuth.AspNetCore # ASP.NET Core apps (brings LumoAuth with it)
using LumoAuth;
using LumoAuth.Models;

var lumo = new LumoAuthClient(new LumoAuthOptions { OrgId = "acme-corp", ApiKey = Environment.GetEnvironmentVariable("LUMOAUTH_API_KEY") });
// or LumoAuthClient.FromEnvironment() — LUMOAUTH_URL, LUMOAUTH_ORG_ID, LUMOAUTH_API_KEY

if (await lumo.Permissions.CheckAsync("document.edit", CheckOptions.ForUser(userId))) { ... }
bool allowed = await lumo.Zanzibar.IsViewerAsync("document:readme", "user:bob");
AbacDecision decision = await lumo.Abac.CheckAsync(new AbacCheck("document", "read", "doc-123"));
ApprovalResult approval = await lumo.Approvals.RequireAsync(new ApprovalRequest
{
TaskId = taskId, Reason = "Deploy", OnBehalfOf = "alice@acme.com", Impact = ApprovalImpact.High,
});

Agents use LumoAgent (client-credentials token kept fresh automatically, RequireCapabilityAsync, CheckBudgetAsync), JitContext (IAsyncDisposable: disposing the task revokes its tokens) and DelegationChain; AAuth agents use LumoAuth.AAuth.AAuthClient, and resource servers verify tokens with AAuthVerifier plus AAuthClaims.VerifyProofOfPossession. Errors are the shared taxonomy as exceptions under LumoAuth.Errors (LumoAuthPermissionDeniedException, LumoAuthRateLimitException with RetryAfter, …).

ASP.NET Core​

{
"LumoAuth": {
"OrgId": "acme-corp",
"ApiKey": "lmk_...",
"Login": { "ClientId": "lumo_abc", "ClientSecret": "..." }
}
}
builder.Services.AddLumoAuth(builder.Configuration);
builder.Services.AddAuthentication(LumoAuthDefaults.BearerScheme).AddLumoAuthBearer(); // APIs
// builder.Services.AddLumoAuthWebAppAuthentication(builder.Configuration); // web apps: cookie + OpenID Connect
builder.Services.AddHealthChecks().AddLumoAuth();

app.MapPost("/invoices/{id}/approve", Approve).RequirePermission("invoice.approve");
app.MapGet("/documents/{id}", GetDocument).RequireRelation("viewer", "document:{id}");

AddLumoAuth registers LumoAuthClient (plus LumoAgent / AAuthClient when their sections are set), the scoped ILumoAuthorization service and the policies behind [RequirePermission], [RequireRelation] and [RequireAbac]. AddLumoAuthBearer resolves LumoAuth bearer tokens to a ClaimsPrincipal (roles and capabilities as claims); AddLumoAuthWebAppAuthentication signs browsers in through the standard OpenID Connect handler with PKCE. See the ASP.NET Core quickstart.

Beyond the wrapped namespaces​

Anything the namespaces do not wrap is reachable with the same credentials through lumo.RequestAsync<T>(...), lumo.RequestJsonAsync(...) and lumo.RequestRawAsync(...); the API reference documents every endpoint.

Standards used on this page​

Plain nameStandard
Signed HTTP requests (HTTP message signatures)RFC 9421
Token exchange (delegation)RFC 8693

See the standards reference for what each one covers and how LumoAuth uses it.