Add authentication to your Express app
This guide walks you through protecting an Express API with LumoAuth using the @lumoauth/sdk TypeScript SDK. You'll validate access tokens, check permissions, and optionally add a server-side login flow.
Before you start
You need:
- A LumoAuth account with a configured tenant (sign up)
- A registered OAuth application with Client ID and Client Secret
- Node.js 18+
From your tenant portal, note your Tenant Slug, Client ID, and Client Secret.
Install dependencies
npm install @lumoauth/sdk express
npm install -D typescript @types/express tsx
Initialize the client
src/lumo.ts
import { LumoAuth } from '@lumoauth/sdk';
export const lumo = new LumoAuth({
baseUrl: process.env.LUMOAUTH_URL || 'https://app.lumoauth.dev',
tenantSlug: process.env.LUMOAUTH_TENANT || 'acme-corp',
clientId: process.env.LUMOAUTH_CLIENT_ID || 'your-client-id',
});
Create auth middleware
Validate the Bearer token on incoming requests and attach the user to the request object.
src/middleware/auth.ts
import type { Request, Response, NextFunction } from 'express';
import { lumo } from '../lumo';
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
try {
const token = header.slice(7);
const user = await lumo.auth.getUserInfo(token);
(req as any).user = user;
(req as any).token = token;
next();
} catch {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
Create permission middleware
Gate routes by RBAC permission using the LumoAuth permissions API.
src/middleware/permissions.ts
import type { Request, Response, NextFunction } from 'express';
import { lumo } from '../lumo';
export function requirePermission(permission: string) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const result = await lumo.permissions.checkDetailed(permission);
if (!result.allowed) {
return res.status(403).json({ error: 'Forbidden', permission });
}
next();
} catch {
res.status(403).json({ error: 'Permission check failed' });
}
};
}
Set up routes
src/index.ts
import express from 'express';
import { requireAuth } from './middleware/auth';
import { requirePermission } from './middleware/permissions';
const app = express();
app.use(express.json());
// Public route
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Authenticated route
app.get('/api/profile', requireAuth, (req, res) => {
res.json((req as any).user);
});
// Permission-protected route
app.get(
'/api/admin/users',
requireAuth,
requirePermission('users.list'),
(req, res) => {
res.json({ users: [] });
}
);
app.delete(
'/api/admin/users/:id',
requireAuth,
requirePermission('users.delete'),
(req, res) => {
res.json({ deleted: req.params.id });
}
);
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server running on port ${port}`));
Add Zanzibar checks (optional)
For relationship-based access control, use the Zanzibar module:
src/routes/documents.ts
import { Router } from 'express';
import { lumo } from '../lumo';
import { requireAuth } from '../middleware/auth';
const router = Router();
router.get('/api/documents/:id', requireAuth, async (req, res) => {
const canView = await lumo.zanzibar.check({
object: `document:${req.params.id}`,
relation: 'viewer',
subject: `user:${(req as any).user.sub}`,
});
if (!canView) {
return res.status(403).json({ error: 'Not authorized to view this document' });
}
res.json({ id: req.params.id, content: '...' });
});
export default router;
Add a server-side login flow (optional)
If your Express app serves HTML pages, you can initiate PKCE login from the server:
src/routes/auth.ts
import { Router } from 'express';
import { lumo } from '../lumo';
const router = Router();
router.get('/auth/login', (req, res) => {
const { url, codeVerifier, state } = lumo.auth.buildAuthorizationUrl({
redirectUri: 'http://localhost:3000/auth/callback',
scope: 'openid profile email',
});
// Store in session (requires express-session)
(req as any).session.codeVerifier = codeVerifier;
(req as any).session.state = state;
res.redirect(url);
});
router.get('/auth/callback', async (req, res) => {
const tokens = await lumo.auth.exchangeCodeForTokens({
code: req.query.code as string,
codeVerifier: (req as any).session.codeVerifier,
redirectUri: 'http://localhost:3000/auth/callback',
});
(req as any).session.accessToken = tokens.access_token;
res.redirect('/dashboard');
});
export default router;
Run the server
npx tsx src/index.ts
Test with curl:
# Health check
curl http://localhost:3000/api/health
# Authenticated request
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
http://localhost:3000/api/profile
# Permission-protected request
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
http://localhost:3000/api/admin/users
What's next?
- Configure RBAC to define roles and permissions for your API
- Set up Zanzibar for relationship-based access control
- Enable webhooks to sync user events with your backend
- SDKs & Libraries - Full SDK API reference