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 app | Install |
|---|---|
| Next.js | npm install @lumoauth/nextjs (server helpers at @lumoauth/nextjs/server) |
| React SPA | npm install @lumoauth/react |
| Vue, Angular, Svelte or other browser app | npm install @lumoauth/client |
| Express | npm install @lumoauth/express |
| Plain Node.js server | npm install @lumoauth/backend |
| JS/TS AI agent (AAuth) | npm install @lumoauth/agent |
| New agent project from scratch | npx create-lumo-agent my-agent |
| Python | pip install lumoauth |
| FastAPI | pip install 'lumoauth[fastapi]' |
| Go | go get github.com/lumoauth/lumo-auth-go |
| PHP | composer require lumoauth/lumoauth — see PHP |
| Symfony | composer require lumoauth/symfony-bundle |
| Laravel | composer require lumoauth/laravel |
| .NET | dotnet add package LumoAuth — see C# / .NET |
| ASP.NET Core | dotnet 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
| Package | Stack | Install |
|---|---|---|
@lumoauth/client | Browser TS/JS — LumoAuth client, OAuth/PKCE flows, RBAC/ABAC/Zanzibar | npm install @lumoauth/client |
@lumoauth/backend | Node.js servers — LumoAuthBackend, same authorization surface plus the lumo.api escape hatch | npm install @lumoauth/backend |
@lumoauth/express | Express middleware: lumoAuthMiddleware() + requireAuth() | npm install @lumoauth/express |
@lumoauth/agent | Node.js AI agents — LumoAgent, AAuth keys, signed HTTP requests, AAuthClient | npm install @lumoauth/agent |
@lumoauth/react | React SPA — <SignIn />, <UserButton />, useLumoAuth() | npm install @lumoauth/react |
@lumoauth/nextjs | Next.js — re-exports the React SDK; server helpers at @lumoauth/nextjs/server | npm install @lumoauth/nextjs |
create-lumo-agent | Scaffolds a Next.js agent app (template nextjs-agent) | npx create-lumo-agent my-agent |
lumoauth | Python — LumoAuth client, agent identity (AAuth), JIT permissions, delegation | pip install lumoauth |
lumoauth[aauth] | Python — Ed25519 signing for AAuth | pip install 'lumoauth[aauth]' |
lumoauth[fastapi] | FastAPI router + get_current_user dependency | pip install 'lumoauth[fastapi]' |
lumo-auth-go | Go — Client, Agent, AAuth, net/http and chi-compatible web sign-in | go get github.com/lumoauth/lumo-auth-go |
lumoauth/lumoauth | PHP 8.1+ — LumoAuth client, AAuth signing, PSR-18 transport | composer require lumoauth/lumoauth |
lumoauth/symfony-bundle | Symfony 6.4 / 7 — configuration, autowired LumoAuth, access_token handler | composer require lumoauth/symfony-bundle |
lumoauth/laravel | Laravel 10–12 — service provider, LumoAuth facade, lumoauth.permission middleware | composer require lumoauth/laravel |
LumoAuth | .NET 8+ — LumoAuthClient, LumoAgent, AAuth signing and verification | dotnet add package LumoAuth |
LumoAuth.AspNetCore | ASP.NET Core 8+ — AddLumoAuth(configuration), bearer tokens + OpenID Connect sign-in, [RequirePermission], health check | dotnet 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.
| Language | Package | Install |
|---|---|---|
| TypeScript | @lumoauth/api-client | npm install @lumoauth/api-client |
| Python | lumoauth-api-client | pip install lumoauth-api-client (or pip install 'lumoauth[api]') |
| Go | github.com/lumoauth/api-clients/go | go 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")
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
| Path | Handler |
|---|---|
GET {prefix}/login | Builds 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}/callback | Verifies 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}/logout | Wipes 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 request | Restores req.user from the session for downstream handlers. |
requireAuth(options)
| Option | Effect |
|---|---|
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: number | Override 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:
| Path | Method | Handler |
|---|---|---|
/auth/login | GET | PKCE authorize redirect, optional ?return_to=/... (same-origin only). |
/auth/callback | GET | Verifies state, exchanges code, persists user+tokens on request.session["__lumoauth"], redirects to return_to. |
/auth/logout | GET / POST | Wipes 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
| Symbol | Purpose |
|---|---|
NewWebAuth(WebConfig) (*WebAuth, error) | Validates config, returns the auth handle. Error on missing BaseURL, Organization, ClientID, SessionSecret. |
(*WebAuth).Router() http.Handler | A 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.Handler | Reads the signed-in user from session into context. Add to chains where you call UserFromContext. |
(*WebAuth).RequireAuth(http.Handler) http.Handler | Gates 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) *Client | A 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) *User | Returns the signed-in user for this request, or nil. |
What WebConfig controls
| Field | Default | Purpose |
|---|---|---|
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/callback | Must match a registered redirect URI |
Scope | openid profile email | Space-separated scopes |
PostLoginRedirect | / | Where to redirect after sign-in |
PostLogoutRedirect | / | Where to redirect after sign-out |
HTTPClient | 10s timeout | Override for tests / instrumentation |
CookieName | lumo_session | Session cookie name |
CookieSecure | true | Secure flag on the session cookie; set lumoauth.Bool(false) only for local development over plain HTTP |
SessionMaxAge | 30 days | Session 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>
);
}
Magic Link & Email-First
Send passwordless magic-link emails and implement email-first login flows using purpose-built hooks.
useMagicLink()
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>
);
}
| Return | Type | Description |
|---|---|---|
sendMagicLink | (email, redirectUri?) => Promise<void> | Send the magic link |
isLoading | boolean | True while the request is in-flight |
isSent | boolean | True after a successful request |
error | string | null | Error message if the request failed |
reset | () => void | Reset 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>
);
}
| Return | Type | Description |
|---|---|---|
checkEmail | (email) => Promise<boolean> | Returns true if account exists |
isLoading | boolean | True while the check is in-flight |
exists | boolean | null | Result of last check (null = not yet checked) |
reset | () => void | Reset 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:
- Next.js Quickstart - Next.js app with
@lumoauth/nextjs - React Quickstart - React SPA with authentication, user components, and access control
- Vue Quickstart, Angular Quickstart and React Native Quickstart -
@lumoauth/clientoutside React - Express Quickstart -
@lumoauth/expresssign-in routes andrequireAuth() - Node.js / Express Quickstart - Express API with token validation and permission checks
- Python Quickstart - Python backend with agent authentication and the Ask API
- FastAPI Quickstart -
lumoauth.fastapirouter and dependencies - Go Quickstart - chi / net/http sign-in with
lumo-auth-go - C# / ASP.NET Core Quickstart - ASP.NET Core app with sign-in, bearer tokens and
[RequirePermission] - create-lumo-agent - scaffold a Next.js agent app in one command
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 argument | Environment variable | Default |
|---|---|---|
baseUrl | LUMOAUTH_URL | https://app.lumoauth.dev |
orgId | LUMOAUTH_ORG_ID | required for org-scoped calls |
apiKey | LUMOAUTH_API_KEY | sent 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');
| Namespace | Example |
|---|---|
permissions | check('document.edit'), checkAny([...]), checkBulk([...]), listSlugs() |
zanzibar | isOwner('document:1', 'user:alice'), expand('document:1', 'viewer') |
abac | isAllowed('document', 'read', 'd1', ['ip' => …]), setUserAttribute(...) |
agents | ask('send_email', $ctx), me(), register(...), capabilities() |
approvals | require(taskId: …, reason: …, onBehalfOf: …)->isApproved() |
jit | createTask(...), request($taskId, $authorizationDetails), token($requestId) |
mcp | getToken('urn:mcp:finance') |
delegation | exchange($subjectToken, $actorToken), Delegation::parseActorChain($jwt) |
auth | authorizationUrl(...), 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 name | Standard |
|---|---|
| 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.