Skip to main content

SDKs & Libraries

LumoAuth provides official, hand-written SDKs for JavaScript/TypeScript, Python, Go, PHP and .NET. It also provides 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 lumo.api (JS) / client.api (Python) for raw API access

@lumoauth/shared (isomorphic core) and @lumoauth/client (framework-agnostic browser client) are the internal packages that 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 raw API access through lumo.apinpm install @lumoauth/backend
@lumoauth/expressExpress middleware: lumoAuthMiddleware() + requireAuth()npm install @lumoauth/express
@lumoauth/agentAI agents on Node.js — 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 stack written in several languages.

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. Use one when you need an endpoint that 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 without extra configuration.


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 lumo.api (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 protect 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, and persists user+tokens on the session. Then 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 needs 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 hide the real bug at sign-in time, when the missing session is most disruptive.

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


FastAPI router​

lumoauth.fastapi — Python FastAPI integration. lumo_auth_router(...) returns an APIRouter that 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.HandlerBlocks 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 that 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 is published as three Composer packages on Packagist. It is 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; takes precedence 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 can then be autowired:

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 that the namespaces do not wrap can be reached 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 is published as two packages on NuGet. It is 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+ (usable 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 that the namespaces do not wrap can be reached 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.