Skip to content

Express & Fastify Adapters

Overview

@authrim/server provides first-class adapters for Express and Fastify. Both adapters validate incoming access tokens (Bearer and DPoP), set authentication context on the request object, and return proper WWW-Authenticate error headers.

Common: MiddlewareOptions

Both adapters accept a shared MiddlewareOptions configuration:

import type { MiddlewareOptions } from '@authrim/server';
const options: MiddlewareOptions = {
/** Realm value for WWW-Authenticate header */
realm: 'my-api',
/** Custom error handler — called when token validation fails */
onError: (error) => {
console.error('Auth error:', error.code, error.message);
},
};
OptionTypeDefaultDescription
realmstringundefinedRealm included in WWW-Authenticate response header
onError(error: AuthrimServerError) => voidundefinedCallback invoked when validation fails (for logging, metrics)

Express Adapter

Import from @authrim/server/adapters/express.

authrimMiddleware()

Required authentication — rejects requests without a valid token (responds with 401):

import express from 'express';
import { createAuthrimServer } from '@authrim/server';
import { authrimMiddleware } from '@authrim/server/adapters/express';
const server = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
const app = express();
// Protect all routes under /api
app.use('/api', authrimMiddleware(server));
app.get('/api/profile', (req, res) => {
// req.auth is guaranteed to be set (ValidatedToken)
res.json({
sub: req.auth.sub,
email: req.auth.email,
tokenType: req.authTokenType, // 'Bearer' | 'DPoP'
});
});

authrimOptionalMiddleware()

Optional authentication — continues even without a token. req.auth is undefined for unauthenticated requests:

import { authrimOptionalMiddleware } from '@authrim/server/adapters/express';
app.get(
'/api/posts',
authrimOptionalMiddleware(server),
(req, res) => {
if (req.auth) {
// Authenticated — show personalized content
res.json({ posts: getPostsForUser(req.auth.sub), user: req.auth.sub });
} else {
// Anonymous — show public content
res.json({ posts: getPublicPosts() });
}
},
);

Request Properties

After middleware runs, these properties are available on req:

PropertyTypeDescription
req.authValidatedToken | undefinedValidated token claims (sub, iss, aud, exp, etc.)
req.authTokenType'Bearer' | 'DPoP' | undefinedToken type used for authentication

Scope-Based Access Control

function requireScope(...scopes: string[]) {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
const tokenScopes = req.auth?.scope?.split(' ') ?? [];
const hasAll = scopes.every((s) => tokenScopes.includes(s));
if (!hasAll) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' ')}`,
});
}
next();
};
}
app.get(
'/api/admin/users',
authrimMiddleware(server),
requireScope('admin:read'),
(req, res) => {
res.json({ users: getAllUsers() });
},
);

Complete Express Example

import express from 'express';
import { createAuthrimServer } from '@authrim/server';
import {
authrimMiddleware,
authrimOptionalMiddleware,
} from '@authrim/server/adapters/express';
const server = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
const app = express();
app.use(express.json());
// Public endpoint — no auth
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Optional auth — enhanced for logged-in users
app.get('/api/feed', authrimOptionalMiddleware(server), (req, res) => {
if (req.auth) {
res.json({ feed: getPersonalizedFeed(req.auth.sub) });
} else {
res.json({ feed: getPublicFeed() });
}
});
// Required auth — protected routes
app.use('/api/me', authrimMiddleware(server));
app.get('/api/me/profile', (req, res) => {
res.json({ sub: req.auth.sub, email: req.auth.email });
});
app.listen(3000, () => console.log('Server running on :3000'));

TypeScript Type Extension

To add proper types for req.auth and req.authTokenType, augment the Express namespace:

import type { ValidatedToken } from '@authrim/server';
declare global {
namespace Express {
interface Request {
auth: ValidatedToken;
authTokenType: 'Bearer' | 'DPoP';
}
}
}

Fastify Adapter

Import from @authrim/server/adapters/fastify.

Fastify offers two integration styles: per-route handlers and app-wide plugins.

Per-Route: authrimPreHandler / authrimOptionalPreHandler

Attach authentication to specific routes using Fastify’s preHandler hook:

import Fastify from 'fastify';
import { createAuthrimServer } from '@authrim/server';
import {
authrimPreHandler,
authrimOptionalPreHandler,
} from '@authrim/server/adapters/fastify';
const server = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
const app = Fastify();
// Required auth on a single route
app.get('/api/profile', {
preHandler: authrimPreHandler(server),
handler: (request, reply) => {
reply.send({
sub: request.auth.sub,
tokenType: request.authTokenType,
});
},
});
// Optional auth on a single route
app.get('/api/posts', {
preHandler: authrimOptionalPreHandler(server),
handler: (request, reply) => {
if (request.auth) {
reply.send({ posts: getPostsForUser(request.auth.sub) });
} else {
reply.send({ posts: getPublicPosts() });
}
},
});

App-Wide: authrimPlugin / authrimOptionalPlugin

Register authentication as a Fastify plugin to protect all routes within a scope:

import {
authrimPlugin,
authrimOptionalPlugin,
} from '@authrim/server/adapters/fastify';
const app = Fastify();
// Protect all routes registered after this plugin
app.register(async (instance) => {
await instance.register(authrimPlugin(server));
instance.get('/api/profile', (request, reply) => {
reply.send({ sub: request.auth.sub });
});
instance.get('/api/settings', (request, reply) => {
reply.send({ settings: getUserSettings(request.auth.sub) });
});
});
// Routes outside the scope are unaffected
app.get('/health', (request, reply) => {
reply.send({ status: 'ok' });
});

Request Properties

PropertyTypeDescription
request.authValidatedToken | undefinedValidated token claims
request.authTokenType'Bearer' | 'DPoP' | undefinedToken type used

Complete Fastify Example

import Fastify from 'fastify';
import { createAuthrimServer } from '@authrim/server';
import {
authrimPreHandler,
authrimOptionalPreHandler,
authrimPlugin,
} from '@authrim/server/adapters/fastify';
const server = createAuthrimServer({
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
const app = Fastify({ logger: true });
// Health check — no auth
app.get('/health', (request, reply) => {
reply.send({ status: 'ok' });
});
// Optional auth route
app.get('/api/feed', {
preHandler: authrimOptionalPreHandler(server),
handler: (request, reply) => {
reply.send({
user: request.auth?.sub ?? 'anonymous',
feed: request.auth ? getPersonalizedFeed(request.auth.sub) : getPublicFeed(),
});
},
});
// Protected scope using plugin
app.register(async (instance) => {
await instance.register(authrimPlugin(server, {
realm: 'my-api',
onError: (err) => app.log.warn({ code: err.code }, 'Auth failed'),
}));
instance.get('/api/profile', (request, reply) => {
reply.send({ sub: request.auth.sub, email: request.auth.email });
});
instance.delete('/api/account', (request, reply) => {
deleteAccount(request.auth.sub);
reply.code(204).send();
});
});
app.listen({ port: 3000 });

TypeScript Module Augmentation

import type { ValidatedToken } from '@authrim/server';
declare module 'fastify' {
interface FastifyRequest {
auth: ValidatedToken;
authTokenType: 'Bearer' | 'DPoP';
}
}

Comparison: Express vs Fastify

AspectExpressFastify
Required auth (per-route)authrimMiddleware(server)authrimPreHandler(server)
Optional auth (per-route)authrimOptionalMiddleware(server)authrimOptionalPreHandler(server)
App-wide authapp.use(authrimMiddleware(server))authrimPlugin(server) / authrimOptionalPlugin(server)
Auth locationreq.authrequest.auth
Token type locationreq.authTokenTyperequest.authTokenType
Type augmentationdeclare global { namespace Express }declare module 'fastify'
Error response401 with WWW-Authenticate401 with WWW-Authenticate

Next Steps