|
1 | | -= Using GraphQL middleware with Neo4j GraphQL |
| 1 | +[[securing-an-api]] |
| 2 | +:description: This page is a tutorial on how to secure your API created with the Neo4j GraphQL Library. |
| 3 | += Securing your GraphQL API |
2 | 4 |
|
3 | | -You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. |
4 | | -This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need. |
| 5 | +Lorem ipsum. |
5 | 6 |
|
| 7 | +== Prerequisites |
6 | 8 |
|
7 | | -== GraphQL middleware |
| 9 | +Lorem ipsum. |
8 | 10 |
|
9 | | -This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. |
10 | | -GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. |
11 | | -It acts as a layer that allows you to apply reusable logic, such as logging, validation, or authentication, across multiple resolvers in a consistent and modular |
12 | | -way. |
| 11 | +== Directives |
13 | 12 |
|
| 13 | +=== Authorization |
14 | 14 |
|
15 | | -=== Logging every request |
| 15 | +Validate versus filter. |
| 16 | +We want to ensure we don't report back database internals to end users. Validate throws an error, which could be a hint of what exists or not. |
16 | 17 |
|
17 | | -Consider this Neo4j GraphQL setup: |
| 18 | +== Further reading |
18 | 19 |
|
19 | | -[source,typescript] |
20 | | ----- |
21 | | -import { ApolloServer } from "@apollo/server"; |
22 | | -import { startStandaloneServer } from "@apollo/server/standalone"; |
23 | | -import { applyMiddleware } from "graphql-middleware"; |
24 | | -import * as neo4j from "neo4j-driver"; |
25 | | -import { Neo4jGraphQL } from "@neo4j/graphql"; |
| 20 | +Explanation on different auth options |
26 | 21 |
|
27 | | -const typeDefs = /* GraphQL */ ` |
28 | | - type User @node { |
29 | | - id: ID! @id |
30 | | - name: String! |
31 | | - email: String! |
32 | | - posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT) |
33 | | - } |
| 22 | +Security best practices |
34 | 23 |
|
35 | | - type Post @node { |
36 | | - id: ID! |
37 | | - title: String! |
38 | | - content: String! |
39 | | - author: [User!]! @relationship(type: "AUTHORED", direction: IN) |
40 | | - } |
41 | | -
|
42 | | - type Query { |
43 | | - me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u") |
44 | | - } |
45 | | -`; |
46 | | -
|
47 | | -const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password")); |
48 | | -
|
49 | | -const neoSchema = new Neo4jGraphQL({ |
50 | | - typeDefs, |
51 | | - driver, |
52 | | -}); |
53 | | -
|
54 | | -const server = new ApolloServer({ |
55 | | - schema: await neoSchema.getSchema(), |
56 | | -}); |
57 | | -
|
58 | | -const { url } = await startStandaloneServer(server, { |
59 | | - listen: { port: 4000 }, |
60 | | -}); |
61 | | -console.log(`🚀 Server ready at ${url}`); |
62 | | ----- |
63 | | - |
64 | | -Add logging to every single operation without touching the generated schema: |
65 | | - |
66 | | -[source,typescript] |
67 | | ----- |
68 | | -import { applyMiddleware } from "graphql-middleware"; |
69 | | -
|
70 | | -/* ...existing code... */ |
71 | | -
|
72 | | -const logMiddleware = async (resolve, root, args, context, info) => { |
73 | | - const start = Date.now(); |
74 | | - console.log(`🚀 ${info.fieldName} started`); |
75 | | -
|
76 | | - try { |
77 | | - const result = await resolve(root, args, context, info); |
78 | | - console.log(`✅ ${info.fieldName} completed in ${Date.now() - start}ms`); |
79 | | - return result; |
80 | | - } catch (error) { |
81 | | - console.log(`💥 ${info.fieldName} failed`); |
82 | | - throw error; |
83 | | - } |
84 | | -}; |
85 | | -
|
86 | | -// Wrap your executable schema |
87 | | -const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { |
88 | | - Query: logMiddleware, |
89 | | - Mutation: logMiddleware, |
90 | | -}); |
91 | | -
|
92 | | -const server = new ApolloServer({ schema: schemaWithLogging }); |
93 | | ----- |
94 | | - |
95 | | -*That’s it. Every query and mutation is now logged.* Your auto-generated |
96 | | -resolver is unchanged, but you’ve added custom behavior. |
97 | | - |
98 | | -Query the users: |
99 | | - |
100 | | -[source,graphql] |
101 | | ----- |
102 | | -{ |
103 | | - users { |
104 | | - name |
105 | | - } |
106 | | -} |
107 | | ----- |
108 | | - |
109 | | -You should see in your server: |
110 | | - |
111 | | -.... |
112 | | -🚀 users started |
113 | | -✅ users completed in 23ms |
114 | | -.... |
115 | | - |
116 | | - |
117 | | -=== Email validation before database writes |
118 | | - |
119 | | -You can use middleware to enforce specific business rules before data is written to the database. |
120 | | -For example, you can ensure that email addresses provided during user creation are valid. |
121 | | -By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. |
122 | | - |
123 | | -Add a middleware that validates the email input in the `createUsers` operation. |
124 | | -A validation error will be thrown before it reaches the Neo4j GraphQL resolver, and the GraphQL client will receive the error message "Invalid email addresses detected". |
125 | | - |
126 | | -[source,typescript] |
127 | | ----- |
128 | | -/* ...existing code... */ |
129 | | -
|
130 | | -const validateEmails = async (resolve, root, args, context, info) => { |
131 | | - // Only check createUsers mutations |
132 | | - if (info.fieldName === "createUsers") { |
133 | | - // Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes. |
134 | | - const invalidEmails = args.input.filter((user) => !user.email.includes("@")); |
135 | | - if (invalidEmails.length > 0) { |
136 | | - throw new Error("Invalid email addresses detected"); |
137 | | - } |
138 | | - } |
139 | | -
|
140 | | - return resolve(root, args, context, info); |
141 | | -}; |
142 | | -
|
143 | | -const schema = applyMiddleware( |
144 | | - await neoSchema.getSchema(), |
145 | | - { |
146 | | - Query: logMiddleware, |
147 | | - Mutation: logMiddleware, |
148 | | - }, |
149 | | - { |
150 | | - Mutation: validateEmails, |
151 | | - } |
152 | | -); |
153 | | ----- |
154 | | - |
155 | | -Try to create a user with the email "not-an-email": |
156 | | - |
157 | | -[source,graphql] |
158 | | ----- |
159 | | -mutation createUsers { |
160 | | - createUsers(input: [{ email: "not-an-email.com", name: "firstname" }]) { |
161 | | - users { |
162 | | - email |
163 | | - } |
164 | | - } |
165 | | -} |
166 | | ----- |
167 | | - |
168 | | - |
169 | | -== Working with Neo4j GraphQL |
170 | | - |
171 | | -Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers. |
172 | | - |
173 | | -Here's the key difference from how traditional GraphQL resolvers are |
174 | | -usually built: |
175 | | - |
176 | | -In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls. |
177 | | -By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query. |
178 | | - |
179 | | -The N+1 problem is solved in Neo4j GraphQL by analyzing the entire GraphQL operation (via the `info` object) and generating optimized Cypher queries that fetch all requested data in a single database round-trip. |
180 | | - |
181 | | -Consider this query: |
182 | | - |
183 | | -[source,graphql] |
184 | | ----- |
185 | | -{ |
186 | | - users { |
187 | | - name |
188 | | - email |
189 | | - posts { |
190 | | - title |
191 | | - content |
192 | | - } |
193 | | - } |
194 | | -} |
195 | | ----- |
196 | | - |
197 | | -Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. |
198 | | -Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. |
199 | | -The nested field resolvers simply return the already fetched data from memory. |
200 | | - |
201 | | - |
202 | | -=== Timing matters |
203 | | - |
204 | | -Timing matters for middleware - by the time the individual field resolvers execute, the database has already been queried and the data is available in the resolver's result. |
205 | | - |
206 | | -Consider the `logMiddleware` from above: |
207 | | - |
208 | | -[source,typescript] |
209 | | ----- |
210 | | -const logMiddleware = async (resolve, root, args, context, info) => { |
211 | | - const start = Date.now(); |
212 | | - console.log(`🚀 ${info.fieldName} started`); |
213 | | -
|
214 | | - try { |
215 | | - const result = await resolve(root, args, context, info); |
216 | | - console.log(`✅ ${info.fieldName} completed in ${Date.now() - start}ms`); |
217 | | - return result; |
218 | | - } catch (error) { |
219 | | - console.log(`💥 ${info.fieldName} failed: ${error.message}`); |
220 | | - throw error; |
221 | | - } |
222 | | -}; |
223 | | ----- |
224 | | - |
225 | | -Apply the `logMiddleware` to queries and the user's name: |
226 | | - |
227 | | -[source,typescript] |
228 | | ----- |
229 | | -const schema = applyMiddleware( |
230 | | - schema, |
231 | | - { |
232 | | - Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip |
233 | | - }, |
234 | | - { |
235 | | - User: { |
236 | | - name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip |
237 | | - }, |
238 | | - } |
239 | | -); |
240 | | ----- |
241 | | - |
242 | | -Run this query: |
243 | | - |
244 | | -[source,graphql] |
245 | | ----- |
246 | | -query { |
247 | | - users { |
248 | | - name |
249 | | - } |
250 | | -} |
251 | | ----- |
252 | | - |
253 | | -You should see: |
254 | | - |
255 | | -.... |
256 | | -🚀 users started |
257 | | -... Neo4j resolver generates and executes Cypher ... |
258 | | -✅ users completed in 48ms |
259 | | -🚀 name started |
260 | | -✅ name completed in 0ms |
261 | | -.... |
262 | | - |
263 | | -Note how the name resolution happens after the round-trip to the database. |
264 | | - |
265 | | -Note the following difference: |
266 | | - |
267 | | -* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers. |
268 | | -* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. |
269 | | - |
270 | | - |
271 | | -== Stack multiple middleware |
272 | | - |
273 | | -It's possible to apply multiple pieces of middleware for the same field. |
274 | | -For instance, you can apply diverse middleware to the same `users` resolver: |
275 | | - |
276 | | -[source,typescript] |
277 | | ----- |
278 | | -const schema = applyMiddleware( |
279 | | - schema, |
280 | | - { |
281 | | - Query: { |
282 | | - users: async (resolve, root, args, context, info) => { |
283 | | - console.log("A started"); |
284 | | - await resolve(root, args, context, info); |
285 | | - console.log("A completed"); |
286 | | - }, |
287 | | - }, |
288 | | - }, |
289 | | - { |
290 | | - Query: { |
291 | | - users: async (resolve, root, args, context, info) => { |
292 | | - console.log("B started"); |
293 | | - await resolve(root, args, context, info); |
294 | | - console.log("B completed"); |
295 | | - }, |
296 | | - }, |
297 | | - }, |
298 | | - { |
299 | | - Query: { |
300 | | - users: async (resolve, root, args, context, info) => { |
301 | | - console.log("C started"); |
302 | | - await resolve(root, args, context, info); |
303 | | - console.log("C completed"); |
304 | | - }, |
305 | | - }, |
306 | | - } |
307 | | -); |
308 | | ----- |
309 | | - |
310 | | -The order in which middleware is applied is important, as they execute one after the other. |
311 | | -Each middleware wraps the next one, creating a chain of execution from outermost to innermost. |
312 | | - |
313 | | -Run this query: |
314 | | - |
315 | | -[source,graphql] |
316 | | ----- |
317 | | -query { |
318 | | - users { |
319 | | - name |
320 | | - } |
321 | | -} |
322 | | ----- |
323 | | - |
324 | | -Schematic output: |
325 | | - |
326 | | -[source,bash] |
327 | | ----- |
328 | | -.... |
329 | | -A started |
330 | | -B started |
331 | | -C started |
332 | | -... Neo4j GraphQL user resolver ... |
333 | | -C completed |
334 | | -B completed |
335 | | -A completed |
336 | | -.... |
337 | | ----- |
338 | | - |
339 | | -The user's resolver is wrapped in three layers of middleware. |
340 | | - |
341 | | - |
342 | | -== Conclusion |
343 | | - |
344 | | -GraphQL middleware with Neo4j GraphQL gives you the best of both worlds: the power of auto-generated schemas and the flexibility to inject custom logic exactly where you need it. |
345 | | - |
346 | | -When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need. |
347 | | - |
348 | | -The GraphQL ecosystem evolves rapidly. |
349 | | -https://the-guild.dev/[The Guild] has developed https://envelop.dev/[Envelop] with its own https://www.npmjs.com/package/@envelop/graphql-middleware[graphql-middleware |
350 | | -plugin]. |
351 | | - |
352 | | -This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. |
353 | | -If you need a more comprehensive plugin ecosystem, we recommend exploring envelop. |
| 24 | +link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] |
0 commit comments