Authentication
How τjs handles route protection and authentication integration.
Overview
Section titled “Overview”τjs provides hooks for authentication but does not implement authentication strategies. You provide the authentication logic, and τjs ensures it runs for protected routes.
Key points:
- Routes declare authentication requirements via
middleware.auth - You implement authentication by decorating Fastify with an
authenticatefunction - τjs calls your
authenticatefunction for protected routes - τjs verifies at startup that authentication is properly configured
Route Protection
Section titled “Route Protection”Mark routes that require authentication using middleware.auth:
{ path: '/dashboard', attr: { render: 'ssr', middleware: { auth: {} // Presence of auth object marks route as protected } }}With metadata:
{ path: '/admin', attr: { render: 'ssr', middleware: { auth: { roles: ['admin'], strategy: 'session' } } }}τjs doesn’t interpret roles or strategy - these are metadata for your authenticate function to read and enforce.
The Authentication Hook
Section titled “The Authentication Hook”When a route has middleware.auth, τjs automatically runs an onRequest hook:
// Internal - τjs does this automaticallyapp.addHook("onRequest", createAuthHook(routeMatchers, logger));For each request, τjs:
- Finds the matching route
- Checks for
middleware.auth - If present, calls
await req.server.authenticate(req, reply) - If
authenticatethrows or returns error, request fails
Implementing Authentication
Section titled “Implementing Authentication”Decorate your Fastify instance with an authenticate function:
import Fastify from "fastify";import { createServer } from "@taujs/server";import config from "./taujs.config";
const fastify = Fastify({ logger: false });
// Define your authenticate functionfastify.decorate("authenticate", async function (req, reply) { // Your authentication logic here const token = req.headers.authorisation?.replace("Bearer ", "");
if (!token) { reply.code(401).send({ error: "Missing token" }); return; }
try { const user = await verifyToken(token); req.user = user; // Attach user to request } catch (err) { reply.code(401).send({ error: "Invalid token" }); }});
// Register τjs serverawait createServer({ fastify, config, serviceRegistry, clientRoot: "./client",});
await fastify.listen({ port: 3000 });What your authenticate function should do:
- Extract credentials (token, session, etc.)
- Verify credentials
- Attach user to request (
req.user) - Send error response if authentication fails
Contract Verification
Section titled “Contract Verification”At startup, τjs verifies that authentication is properly configured:
// τjs checks at startupif (hasProtectedRoutes && !fastify.hasDecorator("authenticate")) { logger.error("Routes require auth but authenticate() is not defined");}Current behavior:
- Logs error if mismatch found
- Does not abort startup
- Returns 500 at runtime if
authenticateis missing
Route Metadata
Section titled “Route Metadata”Your authenticate function can access route metadata to implement custom logic:
fastify.decorate("authenticate", async function (req, reply) { // Route metadata is attached by τjs const routeMeta = (req as any).routeMeta; const authConfig = routeMeta?.middleware?.auth;
// Extract user const user = await verifySession(req);
if (!user) { reply.code(401).send({ error: "Unauthorised" }); return; }
// Check roles if specified const requiredRoles = authConfig?.roles; if ( requiredRoles && !requiredRoles.some((role) => user.roles?.includes(role)) ) { reply.code(403).send({ error: "Forbidden" }); return; }
// Attach user to request req.user = user;});Available in routeMeta:
middleware.auth.roles- Role requirements (if specified)middleware.auth.strategy- Strategy name (if specified)- Any other properties you add to
middleware.auth
Authentication Strategies
Section titled “Authentication Strategies”JWT Authentication
Section titled “JWT Authentication”import fastifyJWT from "@fastify/jwt";
// Register JWT pluginawait fastify.register(fastifyJWT, { secret: process.env.JWT_SECRET,});
// Implement authenticatefastify.decorate("authenticate", async function (req, reply) { try { await req.jwtVerify(); // req.user is populated by @fastify/jwt } catch (err) { reply.code(401).send({ error: "Invalid token" }); }});Session Authentication
Section titled “Session Authentication”import fastifySession from "@fastify/session";import fastifyCookie from "@fastify/cookie";
// Register session pluginsawait fastify.register(fastifyCookie);await fastify.register(fastifySession, { secret: process.env.SESSION_SECRET, cookie: { secure: process.env.NODE_ENV === "production", },});
// Implement authenticatefastify.decorate("authenticate", async function (req, reply) { const userId = req.session.get("userId");
if (!userId) { reply.code(401).send({ error: "Not authenticated" }); return; }
const user = await db.users.findById(userId);
if (!user) { reply.code(401).send({ error: "User not found" }); return; }
req.user = user;});Multiple Strategies
Section titled “Multiple Strategies”Use different authentication strategies per route:
fastify.decorate("authenticate", async function (req, reply) { const routeMeta = (req as any).routeMeta; const strategy = routeMeta?.middleware?.auth?.strategy || "default";
switch (strategy) { case "jwt": await authenticateJWT(req, reply); break; case "session": await authenticateSession(req, reply); break; case "api-key": await authenticateApiKey(req, reply); break; default: reply.code(401).send({ error: "Unknown strategy" }); }});Usage:
// JWT for API routes{ path: '/api/users', attr: { render: 'ssr', middleware: { auth: { strategy: 'jwt' } } }}
// Session for web routes{ path: '/dashboard', attr: { render: 'ssr', middleware: { auth: { strategy: 'session' } } }}Accessing User in Data Handlers
Section titled “Accessing User in Data Handlers”Once authenticated, the user is available in your route’s data handler:
{ path: '/profile', attr: { render: 'ssr', middleware: { auth: {} }, data: async (params, ctx) => { // Access authenticated user through context // (Note: exact implementation depends on how you pass user to ctx) const user = await db.users.findById(ctx.user.id);
return { user }; } }}Accessing User in Services
Section titled “Accessing User in Services”Services receive authenticated user through ServiceContext:
export const ProfileService = defineService({ getCurrentUser: async (params, ctx) => { // ctx.user populated if route has auth middleware if (!ctx.user) { throw new Error("Authentication required"); }
const user = await db.users.findById(ctx.user.id);
return { user }; },
updateProfile: async (params: { name: string }, ctx) => { if (!ctx.user) { throw new Error("Authentication required"); }
const user = await db.users.update({ where: { id: ctx.user.id }, data: { name: params.name }, });
return { user }; },});Best Practices
Section titled “Best Practices”1. Use Secure Session Configuration
Section titled “1. Use Secure Session Configuration”await fastify.register(fastifySession, { secret: process.env.SESSION_SECRET, cookie: { secure: process.env.NODE_ENV === "production", // HTTPS only httpOnly: true, // Prevent XSS sameSite: "lax", // CSRF protection maxAge: 24 * 60 * 60 * 1000, // 24 hours },});2. Validate JWT Properly
Section titled “2. Validate JWT Properly”fastify.decorate("authenticate", async function (req, reply) { try { const decoded = await req.jwtVerify();
// Load fresh user data (don't trust token claims blindly) const user = await db.users.findById(decoded.userId);
if (!user || !user.active) { reply.code(401).send({ error: "User not found or inactive" }); return; }
req.user = user; } catch (err) { reply.code(401).send({ error: "Invalid token" }); }});3. Log Authentication Events
Section titled “3. Log Authentication Events”fastify.decorate("authenticate", async function (req, reply) { try { const user = await verifyAuth(req);
req.log.info( { event: "auth_success", userId: user.id, path: req.url, }, "User authenticated" );
req.user = user; } catch (err) { req.log.warn( { event: "auth_failure", path: req.url, error: err.message, }, "Authentication failed" );
reply.code(401).send({ error: "Unauthorsed" }); }});