@@ -57,23 +57,62 @@ Understanding Fastify's hook execution order is essential for proper security im
5757
5858``` typescript
5959import { requireGlobalAdmin } from ' ../../../middleware/roleMiddleware' ;
60+ import { z } from ' zod' ;
61+ import { createSchema } from ' zod-openapi' ;
6062
61- export default async function secureRoute(fastify : FastifyInstance ) {
62- fastify .post <{ Body: RequestInput }>(' /protected-endpoint' , {
63+ // Define Zod schemas
64+ const RequestSchema = z .object ({
65+ name: z .string ().min (1 , ' Name is required' ),
66+ value: z .string ()
67+ });
68+
69+ const SuccessResponseSchema = z .object ({
70+ success: z .boolean (),
71+ message: z .string ()
72+ });
73+
74+ const ErrorResponseSchema = z .object ({
75+ success: z .boolean ().default (false ),
76+ error: z .string ()
77+ });
78+
79+ export default async function secureRoute(server : FastifyInstance ) {
80+ server .post (' /protected-endpoint' , {
81+ preValidation: requireGlobalAdmin (), // ✅ CORRECT: Runs before validation
6382 schema: {
6483 tags: [' Protected' ],
6584 summary: ' Protected endpoint' ,
6685 description: ' Requires admin permissions' ,
6786 security: [{ cookieAuth: [] }],
68- body: createSchema (RequestSchema ),
87+
88+ // Fastify validation schema
89+ body: {
90+ type: ' object' ,
91+ properties: {
92+ name: { type: ' string' , minLength: 1 },
93+ value: { type: ' string' }
94+ },
95+ required: [' name' , ' value' ],
96+ additionalProperties: false
97+ },
98+
99+ // createSchema() for OpenAPI documentation
100+ requestBody: {
101+ required: true ,
102+ content: {
103+ ' application/json' : {
104+ schema: createSchema (RequestSchema )
105+ }
106+ }
107+ },
108+
69109 response: {
70110 200 : createSchema (SuccessResponseSchema .describe (' Success' )),
71111 401 : createSchema (ErrorResponseSchema .describe (' Unauthorized' )),
72112 403 : createSchema (ErrorResponseSchema .describe (' Forbidden' )),
73113 400 : createSchema (ErrorResponseSchema .describe (' Bad Request' ))
74114 }
75- },
76- preValidation: requireGlobalAdmin (), // ✅ CORRECT: Runs before validation,
115+ }
77116 }, async (request , reply ) => {
78117 // If we reach here, user is authorized AND input is validated
79118 const validatedData = request .body ;
@@ -85,14 +124,19 @@ export default async function secureRoute(fastify: FastifyInstance) {
85124### ❌ Insecure Pattern: preHandler for Authorization
86125
87126``` typescript
88- export default async function insecureRoute(fastify : FastifyInstance ) {
89- fastify .post <{ Body : RequestInput }> (' /protected-endpoint' , {
127+ export default async function insecureRoute(server : FastifyInstance ) {
128+ server .post (' /protected-endpoint' , {
90129 schema: {
91130 // Schema definition...
92- body: zodToJsonSchema (RequestSchema , {
93- $refStrategy: ' none' ,
94- target: ' openApi3'
95- })
131+ body: {
132+ type: ' object' ,
133+ properties: {
134+ name: { type: ' string' , minLength: 1 },
135+ value: { type: ' string' }
136+ },
137+ required: [' name' , ' value' ],
138+ additionalProperties: false
139+ }
96140 },
97141 preHandler: requireGlobalAdmin (), // ❌ WRONG: Runs after validation
98142 }, async (request , reply ) => {
@@ -161,22 +205,24 @@ For endpoints that support both web users (cookies) and CLI users (OAuth2 Bearer
161205``` typescript
162206import { requireAuthenticationAny , requireOAuthScope } from ' ../../middleware/oauthMiddleware' ;
163207
164- fastify .get (' /dual-auth-endpoint' , {
165- schema: {
166- security: [
167- { cookieAuth: [] }, // Cookie authentication
168- { bearerAuth: [] } // OAuth2 Bearer token
169- ]
170- },
171- preValidation: [
172- requireAuthenticationAny (), // Accept either auth method
173- requireOAuthScope (' your:scope' ) // Enforce OAuth2 scope
174- ]
175- }, async (request , reply ) => {
176- // Endpoint accessible via both authentication methods
177- const authType = request .tokenPayload ? ' oauth2' : ' cookie' ;
178- const userId = request .user ! .id ;
179- });
208+ export default async function dualAuthRoute(server : FastifyInstance ) {
209+ server .get (' /dual-auth-endpoint' , {
210+ preValidation: [
211+ requireAuthenticationAny (), // Accept either auth method
212+ requireOAuthScope (' your:scope' ) // Enforce OAuth2 scope
213+ ],
214+ schema: {
215+ security: [
216+ { cookieAuth: [] }, // Cookie authentication
217+ { bearerAuth: [] } // OAuth2 Bearer token
218+ ]
219+ }
220+ }, async (request , reply ) => {
221+ // Endpoint accessible via both authentication methods
222+ const authType = request .tokenPayload ? ' oauth2' : ' cookie' ;
223+ const userId = request .user ! .id ;
224+ });
225+ }
180226```
181227
182228For detailed OAuth2 implementation, see the [ Backend OAuth Implementation Guide] ( /development/backend/oauth-providers ) and [ Backend Security Policy] ( /development/backend/security#oauth2-server-security ) .
@@ -187,29 +233,68 @@ For endpoints that operate within team contexts (e.g., `/teams/:teamId/resource`
187233
188234``` typescript
189235import { requireTeamPermission } from ' ../../../middleware/roleMiddleware' ;
236+ import { z } from ' zod' ;
237+ import { createSchema } from ' zod-openapi' ;
238+
239+ const CreateResourceSchema = z .object ({
240+ name: z .string ().min (1 , ' Name is required' ),
241+ description: z .string ().optional ()
242+ });
243+
244+ const SuccessResponseSchema = z .object ({
245+ success: z .boolean (),
246+ message: z .string ()
247+ });
248+
249+ const ErrorResponseSchema = z .object ({
250+ success: z .boolean ().default (false ),
251+ error: z .string ()
252+ });
190253
191- export default async function teamResourceRoute(fastify : FastifyInstance ) {
192- fastify .post <{
193- Params: { teamId: string };
194- Body: CreateResourceRequest ;
195- }>(' /teams/:teamId/resources' , {
254+ export default async function teamResourceRoute(server : FastifyInstance ) {
255+ server .post (' /teams/:teamId/resources' , {
256+ preValidation: requireTeamPermission (' resources.create' ), // ✅ Team-aware authorization
196257 schema: {
197258 tags: [' Team Resources' ],
198259 summary: ' Create team resource' ,
199260 description: ' Creates a resource within the specified team context' ,
200261 security: [{ cookieAuth: [] }],
201- params: zodToJsonSchema (z .object ({
202- teamId: z .string ().min (1 , ' Team ID is required' )
203- })),
204- body: zodToJsonSchema (CreateResourceSchema ),
262+
263+ params: {
264+ type: ' object' ,
265+ properties: {
266+ teamId: { type: ' string' , minLength: 1 }
267+ },
268+ required: [' teamId' ],
269+ additionalProperties: false
270+ },
271+
272+ body: {
273+ type: ' object' ,
274+ properties: {
275+ name: { type: ' string' , minLength: 1 },
276+ description: { type: ' string' }
277+ },
278+ required: [' name' ],
279+ additionalProperties: false
280+ },
281+
282+ requestBody: {
283+ required: true ,
284+ content: {
285+ ' application/json' : {
286+ schema: createSchema (CreateResourceSchema )
287+ }
288+ }
289+ },
290+
205291 response: {
206- 201 : zodToJsonSchema (SuccessResponseSchema ),
207- 401 : zodToJsonSchema (ErrorResponseSchema .describe (' Unauthorized' )),
208- 403 : zodToJsonSchema (ErrorResponseSchema .describe (' Forbidden - Not team member or insufficient permissions' )),
209- 400 : zodToJsonSchema (ErrorResponseSchema .describe (' Bad Request' ))
292+ 201 : createSchema (SuccessResponseSchema ),
293+ 401 : createSchema (ErrorResponseSchema .describe (' Unauthorized' )),
294+ 403 : createSchema (ErrorResponseSchema .describe (' Forbidden - Not team member or insufficient permissions' )),
295+ 400 : createSchema (ErrorResponseSchema .describe (' Bad Request' ))
210296 }
211- },
212- preValidation: requireTeamPermission (' resources.create' ), // ✅ Team-aware authorization
297+ }
213298 }, async (request , reply ) => {
214299 const { teamId } = request .params ;
215300 const resourceData = request .body ;
@@ -358,21 +443,21 @@ team_user: [
358443
359444``` typescript
360445// Global admin only
361- fastify .delete (' /admin/users/:id' , {
362- schema: { /* ... */ },
446+ server .delete (' /admin/users/:id' , {
363447 preValidation: requireGlobalAdmin (),
448+ schema: { /* ... */ }
364449}, handler );
365450
366451// Specific permission required
367- fastify .post (' /settings/bulk' , {
368- schema: { /* ... */ },
452+ server .post (' /settings/bulk' , {
369453 preValidation: requirePermission (' settings.edit' ),
454+ schema: { /* ... */ }
370455}, handler );
371456
372457// User can access own data OR admin can access any
373- fastify .get (' /users/:id/profile' , {
374- schema: { /* ... */ },
458+ server .get (' /users/:id/profile' , {
375459 preValidation: requireOwnershipOrAdmin (getUserIdFromParams ),
460+ schema: { /* ... */ }
376461}, handler );
377462```
378463
@@ -441,13 +526,13 @@ For complex authorization requirements:
441526
442527``` typescript
443528// Multiple checks in sequence
444- fastify .post (' /complex-endpoint' , {
445- schema: { /* ... */ },
529+ server .post (' /complex-endpoint' , {
446530 preValidation: [
447531 requireAuthentication (), // Must be logged in
448532 requireRole (' team_member' ), // Must have team role
449533 requirePermission (' data.write' ) // Must have write permission
450534 ],
535+ schema: { /* ... */ }
451536}, handler );
452537```
453538
@@ -465,9 +550,9 @@ async function conditionalAuth(request: FastifyRequest, reply: FastifyReply) {
465550 }
466551}
467552
468- fastify .post (' /conditional-endpoint' , {
469- schema: { /* ... */ },
553+ server .post (' /conditional-endpoint' , {
470554 preValidation: conditionalAuth ,
555+ schema: { /* ... */ }
471556}, handler );
472557```
473558
0 commit comments