Skip to content

Express middleware

intelliAuth() is an Express middleware that handles every step of inbound token validation: pulls the bearer token off the request, verifies its signature against the tenant's published key set, checks iss and aud, and surfaces the parsed claims as req.user and req.scopes. Routes that need a specific scope use the requireScope helper.

import { intelliAuth } from '@intelliauth/node-sdk/express'

The default export is the middleware factory.

import express from 'express'
import { intelliAuth } from '@intelliauth/node-sdk/express'
const app = express()
app.use(intelliAuth({
tenantUrl: process.env.INTELLIAUTH_TENANT_URL!,
audience: 'https://api.cymmetri.com',
}))
app.get('/me', (req, res) => {
res.json({ user: req.user, scopes: req.scopes })
})
app.listen(3000)

tenantUrl and audience are required. The middleware fetches the tenant's JWKS document on first use, caches it, and refreshes on kid miss.

OptionRequiredDefaultDescription
tenantUrlyesBase URL of the IntelliAuth tenant.
audienceyesThe expected aud claim. Reject tokens whose audience doesn't match.
issuernotenantUrlOverride the expected iss. Defaults to the tenant URL.
tokenLocationno'header'Where to look for the token: 'header' (Authorization), 'cookie' (a named cookie), or a custom extractor function.
cookieNameno'access_token'If tokenLocation: 'cookie', which cookie to read.
allowUnauthenticatednofalseIf true, pass through unauthenticated requests instead of returning 401. Useful for routes mounted later in the stack.
clockToleranceno30Seconds of clock skew to allow when validating iat / exp.
algorithmsno['RS256']Allowed signing algorithms. Tighten if your tenant only signs with one.
fetchnoglobal fetchOverride the fetch used to fetch JWKS.

After successful validation, the middleware sets:

req.user = {
sub: 'usr_01HZX...',
email: 'finance.lead@cymmetri.com',
email_verified: true,
name: 'Finance Lead',
// ...whatever claims the token carries
}
req.scopes = ['users:read', 'audit:read']
req.token = { /* full parsed claims */ }

Express types are extended via the package's module augmentation; if you don't see autocomplete, add a /// <reference /> line or check your tsconfig.json includes the package.

A per-route guard. Returns a middleware that rejects with 403 if the token doesn't carry the required scope.

import { intelliAuth } from '@intelliauth/node-sdk/express'
app.post('/tenant-admin/users',
intelliAuth.requireScope('users:write'),
(req, res) => { /* ... */ },
)

For "any of these scopes":

intelliAuth.requireScope(['audit:read', 'audit:export'], { mode: 'any' })

For "all of these":

intelliAuth.requireScope(['users:write', 'groups:write'], { mode: 'all' })

mode: 'any' is the default for arrays.

Some routes don't need a specific scope but do need some authenticated user. Use the bare middleware — it already 401s unauthenticated requests when allowUnauthenticated: false (the default). For more nuance:

intelliAuth.requireUser({
emailVerified: true, // also reject users whose email isn't verified
mfaLevel: 'aal2', // also reject sessions below this AAL
})

The middleware produces standardised error shapes:

{
"error": "unauthorized",
"message": "Bearer token missing",
"request_id": "req_01HZX..."
}
{
"error": "insufficient_scope",
"message": "users:write required",
"required_scope": "users:write",
"request_id": "req_01HZX..."
}

For step-up scenarios, return your own 401 with error: "step_up_required" and the required AAL — the step-up topic covers the contract the React SDK expects.

Two patterns:

// Pattern A: gate at router level.
const adminRouter = express.Router()
adminRouter.use(intelliAuth({ tenantUrl, audience }))
adminRouter.use(intelliAuth.requireScope('admin:*'))
app.use('/admin', adminRouter)
// Public routes outside the router stay open.
app.get('/health', (req, res) => res.send('ok'))
// Pattern B: opt-in per route.
const auth = intelliAuth({ tenantUrl, audience, allowUnauthenticated: true })
app.get('/me', auth, intelliAuth.requireUser(), (req, res) => { /* ... */ })
app.get('/health', (req, res) => res.send('ok'))

Pick whichever fits your code's shape. Pattern A is cleaner when most of your surface is authenticated; Pattern B is cleaner when most is public.

This package is Express-specific. For other frameworks, use the underlying verifyAccessToken() primitive exported from @intelliauth/node-sdk and wrap it in your framework's middleware idiom:

import { verifyAccessToken } from '@intelliauth/node-sdk'
const result = await verifyAccessToken(authorizationHeader, {
tenantUrl,
audience,
})
// result.valid, result.user, result.scopes, result.error

This is the same primitive the Express middleware wraps; you get identical validation logic without the Express integration.

The middleware is safe for high-concurrency use. JWKS caching is in-process; a kid miss triggers a single re-fetch shared across concurrent requests. There are no per-request token requests to the platform — validation is local.