@@ -57,23 +57,62 @@ Understanding Fastify's hook execution order is essential for proper security im
57
57
58
58
``` typescript
59
59
import { requireGlobalAdmin } from ' ../../../middleware/roleMiddleware' ;
60
+ import { z } from ' zod' ;
61
+ import { createSchema } from ' zod-openapi' ;
60
62
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
63
82
schema: {
64
83
tags: [' Protected' ],
65
84
summary: ' Protected endpoint' ,
66
85
description: ' Requires admin permissions' ,
67
86
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
+
69
109
response: {
70
110
200 : createSchema (SuccessResponseSchema .describe (' Success' )),
71
111
401 : createSchema (ErrorResponseSchema .describe (' Unauthorized' )),
72
112
403 : createSchema (ErrorResponseSchema .describe (' Forbidden' )),
73
113
400 : createSchema (ErrorResponseSchema .describe (' Bad Request' ))
74
114
}
75
- },
76
- preValidation: requireGlobalAdmin (), // ✅ CORRECT: Runs before validation,
115
+ }
77
116
}, async (request , reply ) => {
78
117
// If we reach here, user is authorized AND input is validated
79
118
const validatedData = request .body ;
@@ -85,14 +124,19 @@ export default async function secureRoute(fastify: FastifyInstance) {
85
124
### ❌ Insecure Pattern: preHandler for Authorization
86
125
87
126
``` 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' , {
90
129
schema: {
91
130
// 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
+ }
96
140
},
97
141
preHandler: requireGlobalAdmin (), // ❌ WRONG: Runs after validation
98
142
}, async (request , reply ) => {
@@ -161,22 +205,24 @@ For endpoints that support both web users (cookies) and CLI users (OAuth2 Bearer
161
205
``` typescript
162
206
import { requireAuthenticationAny , requireOAuthScope } from ' ../../middleware/oauthMiddleware' ;
163
207
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
+ }
180
226
```
181
227
182
228
For 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`
187
233
188
234
``` typescript
189
235
import { 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
+ });
190
253
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
196
257
schema: {
197
258
tags: [' Team Resources' ],
198
259
summary: ' Create team resource' ,
199
260
description: ' Creates a resource within the specified team context' ,
200
261
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
+
205
291
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' ))
210
296
}
211
- },
212
- preValidation: requireTeamPermission (' resources.create' ), // ✅ Team-aware authorization
297
+ }
213
298
}, async (request , reply ) => {
214
299
const { teamId } = request .params ;
215
300
const resourceData = request .body ;
@@ -358,21 +443,21 @@ team_user: [
358
443
359
444
``` typescript
360
445
// Global admin only
361
- fastify .delete (' /admin/users/:id' , {
362
- schema: { /* ... */ },
446
+ server .delete (' /admin/users/:id' , {
363
447
preValidation: requireGlobalAdmin (),
448
+ schema: { /* ... */ }
364
449
}, handler );
365
450
366
451
// Specific permission required
367
- fastify .post (' /settings/bulk' , {
368
- schema: { /* ... */ },
452
+ server .post (' /settings/bulk' , {
369
453
preValidation: requirePermission (' settings.edit' ),
454
+ schema: { /* ... */ }
370
455
}, handler );
371
456
372
457
// User can access own data OR admin can access any
373
- fastify .get (' /users/:id/profile' , {
374
- schema: { /* ... */ },
458
+ server .get (' /users/:id/profile' , {
375
459
preValidation: requireOwnershipOrAdmin (getUserIdFromParams ),
460
+ schema: { /* ... */ }
376
461
}, handler );
377
462
```
378
463
@@ -441,13 +526,13 @@ For complex authorization requirements:
441
526
442
527
``` typescript
443
528
// Multiple checks in sequence
444
- fastify .post (' /complex-endpoint' , {
445
- schema: { /* ... */ },
529
+ server .post (' /complex-endpoint' , {
446
530
preValidation: [
447
531
requireAuthentication (), // Must be logged in
448
532
requireRole (' team_member' ), // Must have team role
449
533
requirePermission (' data.write' ) // Must have write permission
450
534
],
535
+ schema: { /* ... */ }
451
536
}, handler );
452
537
```
453
538
@@ -465,9 +550,9 @@ async function conditionalAuth(request: FastifyRequest, reply: FastifyReply) {
465
550
}
466
551
}
467
552
468
- fastify .post (' /conditional-endpoint' , {
469
- schema: { /* ... */ },
553
+ server .post (' /conditional-endpoint' , {
470
554
preValidation: conditionalAuth ,
555
+ schema: { /* ... */ }
471
556
}, handler );
472
557
```
473
558
0 commit comments