Middleware: Auth
Authentication is one of the most common needs in web backends, and also one of the easiest to mess up.
That’s why TriFrost ships first-class auth middleware covering the most common patterns, with consistent behavior, minimal configuration, and safe defaults.
Overview
All TriFrost auth middleware follow the same shape:
1. Extract credential (header, query, cookie, etc).
2. Run interval validation, for example verify cookie, auth structure, etc.
3. Run your validate() function to check against secrets, DBs, etc.
4. Attach to context if valid, sets ctx.state.$auth.
5. Fail fast if invalid, immediately short-circuits the chain and returns a 401 Unauthorized.
This design gives you a consistent, composable auth pattern across all entry points.
$auth object
The $auth object can be as simple or rich as you want — each middleware offers a validate() method which can return a boolean (true = valid) or a full object (object means valid).
This object is added to the state of the context and as such in addition to auth guarding your routes it can also enrich context for usage further down the line (for example, loading a session).
Example, enriching $auth with a loaded user:
import {SessionCookieAuth} from '@trifrost/core';
app.group('/account', router => {
router
.use(SessionCookieAuth({
cookie: 'session_id',
secret: {val: ctx => ctx.env.SESSION_SECRET},
validate: async (ctx, sessionId) => {
const session = await ctx.cache.get('session:' + sessionId);
if (!session) return false;
const user = await ctx.cache.get<{
name: string;
email: string;
settings: Record<string, unknown>
}>('user:' + session.userId);
if (!user) return false;
return user;
},
}))
.get('/profile', ctx => ctx.json({
message: `Welcome back, ${ctx.state.$auth.name}`,
email: ctx.state.$auth.email
}))
.get('/settings', ctx => ctx.json({
settings: ctx.state.$auth.settings
}))
});What’s happening here:
- We attach
SessionCookieAuthto the entire/accountgroup. - The
validate()function does a lookup to check if the session and then user exists and returns either false or the user - All subroutes (
/profile,/settings, etc.) can now safely accessctx.state.$auth.
Note: No extra checks are needed in downstream handler given that you only get into a handler guarded by auth middleware if auth passes.
Supported Middleware
With zero extra dependencies you can currently drop in:
- ApiKeyAuth: Api key authentication
- BasicAuth: Basic username/password auth
- BearerAuth: Bearer token auth
- SessionCookieAuth: HMAC-signed session cookie auth
Each middleware is:
- Easy to attach globally, per group, or per route (
app.use(),router.use(),route.use(), etc.) - Explicit: it extracts, validates, and attaches a clean
$authobject toctx.state - Fail-fast: on auth failure, it immediately returns a
401 Unauthorized, no extra checks needed
Which one should you use?
- ApiKeyAuth: When external systems or services need to access your API with a static API key.
- BasicAuth: When you want simple username/password access (often internal admin tools).
- BearerAuth: When working with OAuth, JWT, or third-party tokens.
- SessionCookieAuth: When managing logged-in user sessions in a web app, tied to HMAC-signed cookies.
🔑 ApiKeyAuth
The ApiKeyAuth middleware lets you protect routes using API keys passed in headers or query parameters.
Options
apiKey: {header?:string; query?:string}
Where to extract the API keyapiClient(optional): {header?:string; query?:string}
Where to extract the client or app identifiervalidate(ctx, {apiKey, apiClient}): function
Your validation function. Returntrue(valid) or a custom object to inject into$auth; returnfalseto reject.
Note: If
validatereturntrueinstead of an object the$authobject will be in the shape of{apiClient:string, apiClient:string|null}. You are expected to explicitly check both parts if you configure both.
Example:
import {ApiKeyAuth} from '@trifrost/core';
app.group('/partner-api', router => {
router
.use(ApiKeyAuth({
apiKey: {header: 'x-api-key'},
apiClient: {header: 'x-api-client'},
validate: async (ctx, {apiKey, apiClient}) => {
const client = await myApiKeyStore.checkClientKeyPair(apiClient!, apiKey);
if (!client) return false;
return {
clientId: apiClient,
plan: client.plan,
limit: client.limit,
};
}
}))
.limit(ctx => ctx.state.$auth.limit)
.get('/data', ctx => ctx.json({
client: ctx.state.$auth.clientId,
plan: ctx.state.$auth.plan,
message: 'Checked against rate limit and authenticated!',
}));
});What’s happening here:
- We explicitly configure both
apiKeyandapiClientheaders. - The
validate()function checks the pair and returns enriched$authcontext - We make use of the
$authobject for dynamic per-client rate limiting using.limit(). - Downstream routes can safely access all the enriched details.
Note:: Remember to include your configured headers as allowed headers on Cors if you are using
ApiKeyAuthand Cors on the same app.
🧑💼 BasicAuth
The BasicAuth middleware uses HTTP Basic Auth headers to authenticate with username + password.
Options
realm: string
Realm label forWWW-Authenticateheader.
default:'Restricted Area'validate(ctx, {user, pass}): function
Your validation function. Returntrue(valid) or a custom object to inject into$auth; returnfalseto reject.
Note:
- On failure it returns401 Unauthorizedas well as setsWWW-Authenticate.
- Ifvalidatereturntrueinstead of an object the$authobject will be in the shape of{user:string}.
Example:
import {BasicAuth} from '@trifrost/core';
app.group('/admin', router => {
router
.use(BasicAuth({
validate: (ctx, {user, pass}) =>
user === 'admin' && pass === ctx.env.ADMIN_SECRET
? {role: 'admin'}
: false
}))
.get('/dashboard', ctx => {
return ctx.json({
message: `Welcome, ${ctx.state.$auth.role}!`
});
});
});What’s happening here:
- We protect
/adminwith basic auth. - Only
admin+ secret password is allowed. $authcontains{role: 'admin'}.
🏷️ BearerAuth
The BearerAuth middleware authenticates requests using bearer tokens in the Authorization header.
Options
validate(ctx, token): function
Your validation function. Returntrue(valid) or a custom object to inject into$auth; returnfalseto reject.
Note:
- Ifvalidatereturntrueinstead of an object the$authobject will be in the shape of{token:string}.
Example:
import {BearerAuth} from '@trifrost/core';
app.group('/protected', router => {
router
.use(BearerAuth({
validate: async (ctx, token) => {
/* Note: At time of writing TriFrost does not yet include a JWT module, but soon ... will ;) */
const decoded = await verifyJwt(token, ctx.env.JWT_SECRET);
return decoded ? {userId: decoded.sub, scopes: decoded.scopes} : false;
}
}))
.get('/data', ctx => {
return ctx.json({
userId: ctx.state.$auth.userId,
scopes: ctx.state.$auth.scopes
});
});
});What’s happening here:
- We decode and validate the JWT token.
- If valid, we enrich
$authwith the user + scopes.
🍪 SessionCookieAuth
The SessionCookieAuth middleware authenticates requests using signed session cookies.
Options
cookie: string
Name of the cookie holding the signed session valuesecret: {val, algorithm?}
Secret used to verify HMAC signature. Accepts a string or a function returning a string.validate(ctx, value): function (optional)
Additional validation after verifying the signature. Returntrueor an object to attach to$auth.
Note:
- Ifvalidateis not defined or returnstrueinstead of an object the$authobject will be in the shape of{cookie:string}.
Example:
import {SessionCookieAuth} from '@trifrost/core';
app.group('/account', router => {
router
.use(SessionCookieAuth({
cookie: 'session_id',
secret: {val: ctx => ctx.env.SESSION_SECRET},
validate: async (ctx, sessionId) => {
const session = await ctx.cache.get('session:' + sessionId);
if (!session) return false;
const user = await ctx.cache.get<{
name: string;
email: string;
settings: Record<string, unknown>
}>('user:' + session.userId);
if (!user) return false;
return user;
}
}))
.get('/profile', ctx => ctx.json({
message: `Welcome back, ${ctx.state.$auth.name}`,
email: ctx.state.$auth.email
}));
});What’s happening here:
- We check the signed
session_idcookie. - If valid, we load the user and attach it to
$auth.
Best Practices
- ✅ Always store secrets (API keys, passwords, etc.) in
ctx.envor a database — never hardcode. - ✅ When enriching
$auth, only pass what’s needed (avoid leaking sensitive details). - ✅ Monitor + log failures carefully, but don’t expose sensitive info in responses.
- ✅ Use HMAC-signing (
SessionCookieAuth) for sensitive info — never trust client-side blindly. - ✅ Apply auth per route, group, or globally — TriFrost’s middleware chain makes this easy.
- ✅ Avoid mixing auth layers on the same route — pick one clear mechanism per entry point.
- ✅ Structure your
$authobjects consistently to make downstream code simpler. - ✅ Always validate tokens/sessions/keys with authoritative sources (not just local comparison).