From f4ab1d1ca32d9acf82d2ad78f28c26a9713357bb Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Wed, 30 Jul 2025 13:59:44 +0200 Subject: [PATCH 01/13] converted graphql middleware blog post --- .../security/securing-a-graphql-api.adoc | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 modules/ROOT/pages/security/securing-a-graphql-api.adoc diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc new file mode 100644 index 00000000..de26f209 --- /dev/null +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -0,0 +1,353 @@ += Using GraphQL middleware with Neo4j GraphQL + +You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. +This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need. + + +== GraphQL middleware + +This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. +GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. +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 +way. + + +=== Logging every request + +Consider this Neo4j GraphQL setup: + +[source,typescript] +---- +import { ApolloServer } from "@apollo/server"; +import { startStandaloneServer } from "@apollo/server/standalone"; +import { applyMiddleware } from "graphql-middleware"; +import * as neo4j from "neo4j-driver"; +import { Neo4jGraphQL } from "@neo4j/graphql"; + +const typeDefs = /* GraphQL */ ` + type User @node { + id: ID! @id + name: String! + email: String! + posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT) + } + + type Post @node { + id: ID! + title: String! + content: String! + author: [User!]! @relationship(type: "AUTHORED", direction: IN) + } + + type Query { + me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u") + } +`; + +const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password")); + +const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, +}); + +const server = new ApolloServer({ + schema: await neoSchema.getSchema(), +}); + +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, +}); +console.log(`πŸš€ Server ready at ${url}`); +---- + +Add logging to every single operation without touching the generated schema: + +[source,typescript] +---- +import { applyMiddleware } from "graphql-middleware"; + +/* ...existing code... */ + +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed`); + throw error; + } +}; + +// Wrap your executable schema +const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { + Query: logMiddleware, + Mutation: logMiddleware, +}); + +const server = new ApolloServer({ schema: schemaWithLogging }); +---- + +*That’s it. Every query and mutation is now logged.* Your auto-generated +resolver is unchanged, but you’ve added custom behavior. + +Query the users: + +[source,graphql] +---- +{ + users { + name + } +} +---- + +You should see in your server: + +.... +πŸš€ users started +βœ… users completed in 23ms +.... + + +=== Email validation before database writes + +You can use middleware to enforce specific business rules before data is written to the database. +For example, you can ensure that email addresses provided during user creation are valid. +By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. + +Add a middleware that validates the email input in the `createUsers` operation. +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". + +[source,typescript] +---- +/* ...existing code... */ + +const validateEmails = async (resolve, root, args, context, info) => { + // Only check createUsers mutations + if (info.fieldName === "createUsers") { + // Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes. + const invalidEmails = args.input.filter((user) => !user.email.includes("@")); + if (invalidEmails.length > 0) { + throw new Error("Invalid email addresses detected"); + } + } + + return resolve(root, args, context, info); +}; + +const schema = applyMiddleware( + await neoSchema.getSchema(), + { + Query: logMiddleware, + Mutation: logMiddleware, + }, + { + Mutation: validateEmails, + } +); +---- + +Try to create a user with the email "not-an-email": + +[source,graphql] +---- +mutation createUsers { + createUsers(input: [{ email: "not-an-email.com", name: "firstname" }]) { + users { + email + } + } +} +---- + + +== Working with Neo4j GraphQL + +Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers. + +Here's the key difference from how traditional GraphQL resolvers are +usually built: + +In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls. +By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query. + +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. + +Consider this query: + +[source,graphql] +---- +{ + users { + name + email + posts { + title + content + } + } +} +---- + +Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. +Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. +The nested field resolvers simply return the already fetched data from memory. + + +=== Timing matters + +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. + +Consider the `logMiddleware` from above: + +[source,typescript] +---- +const logMiddleware = async (resolve, root, args, context, info) => { + const start = Date.now(); + console.log(`πŸš€ ${info.fieldName} started`); + + try { + const result = await resolve(root, args, context, info); + console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); + return result; + } catch (error) { + console.log(`πŸ’₯ ${info.fieldName} failed: ${error.message}`); + throw error; + } +}; +---- + +Apply the `logMiddleware` to queries and the user's name: + +[source,typescript] +---- +const schema = applyMiddleware( + schema, + { + Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip + }, + { + User: { + name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip + }, + } +); +---- + +Run this query: + +[source,graphql] +---- +query { + users { + name + } +} +---- + +You should see: + +.... +πŸš€ users started +... Neo4j resolver generates and executes Cypher ... +βœ… users completed in 48ms +πŸš€ name started +βœ… name completed in 0ms +.... + +Note how the name resolution happens after the round-trip to the database. + +Note the following difference: + +* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers. +* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. + + +== Stack multiple middleware + +It's possible to apply multiple pieces of middleware for the same field. +For instance, you can apply diverse middleware to the same `users` resolver: + +[source,typescript] +---- +const schema = applyMiddleware( + schema, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("A started"); + await resolve(root, args, context, info); + console.log("A completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("B started"); + await resolve(root, args, context, info); + console.log("B completed"); + }, + }, + }, + { + Query: { + users: async (resolve, root, args, context, info) => { + console.log("C started"); + await resolve(root, args, context, info); + console.log("C completed"); + }, + }, + } +); +---- + +The order in which middleware is applied is important, as they execute one after the other. +Each middleware wraps the next one, creating a chain of execution from outermost to innermost. + +Run this query: + +[source,graphql] +---- +query { + users { + name + } +} +---- + +Schematic output: + +[source,bash] +---- +.... +A started +B started +C started +... Neo4j GraphQL user resolver ... +C completed +B completed +A completed +.... +---- + +The user's resolver is wrapped in three layers of middleware. + + +== Conclusion + +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. + +When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need. + +The GraphQL ecosystem evolves rapidly. +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 +plugin]. + +This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. +If you need a more comprehensive plugin ecosystem, we recommend exploring envelop. \ No newline at end of file From c35374fe50997f96d52cb5f8b8cb23def502cc60 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Mon, 25 Aug 2025 15:04:44 +0200 Subject: [PATCH 02/13] changes --- .../security/securing-a-graphql-api.adoc | 357 +----------------- 1 file changed, 14 insertions(+), 343 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index de26f209..893c96aa 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -1,353 +1,24 @@ -= Using GraphQL middleware with Neo4j GraphQL +[[securing-an-api]] +:description: This page is a tutorial on how to secure your API created with the Neo4j GraphQL Library. += Securing your GraphQL API -You can wrap your auto-generated Neo4j GraphQL resolver with custom logic that intercepts specific GraphQL operations. -This approach allows you to retain all the benefits of auto-generation while adding the custom behavior you need. +Lorem ipsum. +== Prerequisites -== GraphQL middleware +Lorem ipsum. -This page makes use of https://github.com/dimatill/graphql-middleware[`graphql-middleware`]. -GraphQL middleware is a library that provides a way to wrap and extend the behavior of your GraphQL resolvers. -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 -way. +== Directives +=== Authorization -=== Logging every request +Validate versus filter. +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. -Consider this Neo4j GraphQL setup: +== Further reading -[source,typescript] ----- -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; -import { applyMiddleware } from "graphql-middleware"; -import * as neo4j from "neo4j-driver"; -import { Neo4jGraphQL } from "@neo4j/graphql"; +Explanation on different auth options -const typeDefs = /* GraphQL */ ` - type User @node { - id: ID! @id - name: String! - email: String! - posts: [Post!]! @relationship(type: "AUTHORED", direction: OUT) - } +Security best practices - type Post @node { - id: ID! - title: String! - content: String! - author: [User!]! @relationship(type: "AUTHORED", direction: IN) - } - - type Query { - me: User @cypher(statement: "MATCH (u:User {id: $userId}) RETURN u", columnName: "u") - } -`; - -const driver = neo4j.driver("bolt://localhost:7687", neo4j.auth.basic("neo4j", "password")); - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - driver, -}); - -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -const { url } = await startStandaloneServer(server, { - listen: { port: 4000 }, -}); -console.log(`πŸš€ Server ready at ${url}`); ----- - -Add logging to every single operation without touching the generated schema: - -[source,typescript] ----- -import { applyMiddleware } from "graphql-middleware"; - -/* ...existing code... */ - -const logMiddleware = async (resolve, root, args, context, info) => { - const start = Date.now(); - console.log(`πŸš€ ${info.fieldName} started`); - - try { - const result = await resolve(root, args, context, info); - console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); - return result; - } catch (error) { - console.log(`πŸ’₯ ${info.fieldName} failed`); - throw error; - } -}; - -// Wrap your executable schema -const schemaWithLogging = applyMiddleware(await neoSchema.getSchema(), { - Query: logMiddleware, - Mutation: logMiddleware, -}); - -const server = new ApolloServer({ schema: schemaWithLogging }); ----- - -*That’s it. Every query and mutation is now logged.* Your auto-generated -resolver is unchanged, but you’ve added custom behavior. - -Query the users: - -[source,graphql] ----- -{ - users { - name - } -} ----- - -You should see in your server: - -.... -πŸš€ users started -βœ… users completed in 23ms -.... - - -=== Email validation before database writes - -You can use middleware to enforce specific business rules before data is written to the database. -For example, you can ensure that email addresses provided during user creation are valid. -By using middleware, you can intercept and validate the input before it reaches the Neo4j GraphQL resolver. - -Add a middleware that validates the email input in the `createUsers` operation. -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". - -[source,typescript] ----- -/* ...existing code... */ - -const validateEmails = async (resolve, root, args, context, info) => { - // Only check createUsers mutations - if (info.fieldName === "createUsers") { - // Note: This is a simplistic and intentionally flawed email validation example, but good for demonstration purposes. - const invalidEmails = args.input.filter((user) => !user.email.includes("@")); - if (invalidEmails.length > 0) { - throw new Error("Invalid email addresses detected"); - } - } - - return resolve(root, args, context, info); -}; - -const schema = applyMiddleware( - await neoSchema.getSchema(), - { - Query: logMiddleware, - Mutation: logMiddleware, - }, - { - Mutation: validateEmails, - } -); ----- - -Try to create a user with the email "not-an-email": - -[source,graphql] ----- -mutation createUsers { - createUsers(input: [{ email: "not-an-email.com", name: "firstname" }]) { - users { - email - } - } -} ----- - - -== Working with Neo4j GraphQL - -Most of the above is applicable even outside Neo4j GraphQL, but there is an important concept when writing middleware for Neo4j GraphQL resolvers. - -Here's the key difference from how traditional GraphQL resolvers are -usually built: - -In *traditional GraphQL*, each field resolver executes independently, potentially causing multiple database calls. -By contrast, in *Neo4j GraphQL* the root field resolver (like `users` or `createUsers`) analyzes the entire query tree and executes one optimized Cypher query. - -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. - -Consider this query: - -[source,graphql] ----- -{ - users { - name - email - posts { - title - content - } - } -} ----- - -Neo4j GraphQL doesn't execute separate resolvers for `name`, `email`, `posts`, `title`, and `content`. -Instead, the `users` field resolver generates and executes a single Cypher query that returns all the data at once. -The nested field resolvers simply return the already fetched data from memory. - - -=== Timing matters - -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. - -Consider the `logMiddleware` from above: - -[source,typescript] ----- -const logMiddleware = async (resolve, root, args, context, info) => { - const start = Date.now(); - console.log(`πŸš€ ${info.fieldName} started`); - - try { - const result = await resolve(root, args, context, info); - console.log(`βœ… ${info.fieldName} completed in ${Date.now() - start}ms`); - return result; - } catch (error) { - console.log(`πŸ’₯ ${info.fieldName} failed: ${error.message}`); - throw error; - } -}; ----- - -Apply the `logMiddleware` to queries and the user's name: - -[source,typescript] ----- -const schema = applyMiddleware( - schema, - { - Query: logMiddleware, // wraps all the Queries and it's executed before the database round-trip - }, - { - User: { - name: logMiddleware, // wraps only the User's name field resolver and it's executed after the database roundtrip - }, - } -); ----- - -Run this query: - -[source,graphql] ----- -query { - users { - name - } -} ----- - -You should see: - -.... -πŸš€ users started -... Neo4j resolver generates and executes Cypher ... -βœ… users completed in 48ms -πŸš€ name started -βœ… name completed in 0ms -.... - -Note how the name resolution happens after the round-trip to the database. - -Note the following difference: - -* Query and mutation level middleware runs before and after the Neo4j GraphQL autogenerated resolvers. -* Type and field level middleware runs only after the Neo4j GraphQL autogenerated resolvers. - - -== Stack multiple middleware - -It's possible to apply multiple pieces of middleware for the same field. -For instance, you can apply diverse middleware to the same `users` resolver: - -[source,typescript] ----- -const schema = applyMiddleware( - schema, - { - Query: { - users: async (resolve, root, args, context, info) => { - console.log("A started"); - await resolve(root, args, context, info); - console.log("A completed"); - }, - }, - }, - { - Query: { - users: async (resolve, root, args, context, info) => { - console.log("B started"); - await resolve(root, args, context, info); - console.log("B completed"); - }, - }, - }, - { - Query: { - users: async (resolve, root, args, context, info) => { - console.log("C started"); - await resolve(root, args, context, info); - console.log("C completed"); - }, - }, - } -); ----- - -The order in which middleware is applied is important, as they execute one after the other. -Each middleware wraps the next one, creating a chain of execution from outermost to innermost. - -Run this query: - -[source,graphql] ----- -query { - users { - name - } -} ----- - -Schematic output: - -[source,bash] ----- -.... -A started -B started -C started -... Neo4j GraphQL user resolver ... -C completed -B completed -A completed -.... ----- - -The user's resolver is wrapped in three layers of middleware. - - -== Conclusion - -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. - -When you need custom logic, graphql-middleware lets you keep the rapid development benefits of Neo4j GraphQL while adding the custom behavior you need. - -The GraphQL ecosystem evolves rapidly. -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 -plugin]. - -This guide uses `graphql-middleware` because it's server-agnostic and delivers the clearest path to understanding middleware with Neo4j GraphQL. -If you need a more comprehensive plugin ecosystem, we recommend exploring envelop. \ No newline at end of file +link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] \ No newline at end of file From 2ccaca9b73414416550a6221967e7763c9084fa6 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Thu, 18 Sep 2025 16:34:44 +0200 Subject: [PATCH 03/13] structure + some text, missing examples --- .../security/securing-a-graphql-api.adoc | 217 +++++++++++++++++- 1 file changed, 212 insertions(+), 5 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index 893c96aa..a4544851 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -4,21 +4,228 @@ Lorem ipsum. + == Prerequisites +Set up a new AuraDB instance. +Refer to link:https://neo4j.com/docs/aura/getting-started/create-instance/[Creating a Neo4j Aura instance]. + + +== Security-related directives + +Lorem ipsum. + + +=== Authentication + Lorem ipsum. +Explanation on different auth options. + + +==== JWT + +Lorem ipsum. + + +==== Lorem? + +ipsum. -== Directives === Authorization Validate versus filter. 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. -== Further reading -Explanation on different auth options +== Best practice checklist + +Besides authentication and authorization considerations, there are a couple of worthwhile best practices to increase your API's security. + + +=== Avoid introspection and data field suggestions + +While the xref:getting-started/graphql-aura.adoc[Getting started page for GraphQL and Aura Console] advocates to both **Enable introspection** as well as **Enable field suggestions**, this is not recommended when considering security. + +Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious opLorem ipsumerations. +Be sure to deactivate both in a customer-facing real-life scenario. + + +=== Catch internal errors + +For the same reason it is advisable to avoid introspection and data field suggestions, it can make your API more secure to catch internal errors and redact which information you want to pass on to the end user. + +For example, the following error reveals information XY: + +[source, json, indent=0] +---- +"data": { + "field": "value" +} +---- + +You can use Apollo Server's link:https://www.apollographql.com/docs/apollo-server/data/errors[Error Handling] to catch such internal errors and then decide how to pass this on to your users: + +[source, typescript, indent=0] +---- +import { ApolloServerErrorCode } from '@apollo/server/errors'; + +if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED) { + // respond to the syntax error + +} else if (error.extensions?.code === "MY_CUSTOM_CODE") { + // do something else + +} +---- + + +=== Limit query depth + +Limiting query depth disallows potentially harmful queries such as the following recursive query: + +[source, graphql, indent=0] +---- +query { + order(id: 42) { + products { + order { + products { + order { + products { + order { + # and so on... + } + } + } + } + } + } + } +} +---- + +This can be achieved with a package such as link:https://www.npmjs.com/package/graphql-depth-limit[graphql-depth-limit]: + +[source, typescript, indent=0] +---- +import depthLimit from 'graphql-depth-limit' +import express from 'express' +import graphqlHTTP from 'express-graphql' +import schema from './schema' + +const app = express() + +app.use('/graphql', graphqlHTTP((req, res) => ({ + schema, + validationRules: [ depthLimit(10) ] +}))) +---- -Security best practices -link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] \ No newline at end of file +=== Paginate list fields + +Returning large query result lists can negatively affect server performance. +For example, a query like the following would return a siginificant number of nodes: + +[source, graphql, indent=0] +---- +query { + order(first: 1000) { + orderID + products(last: 100) { + productName + productCategory + } + } +} +---- + +To avoid this, you can cap the input number directly in your resolvers, for example like this: + +[source, graphql, indent=0] +---- +// example +---- + +Alternatively, use a library such as link:https://github.com/joonhocho/graphql-input-number[graphql-input-number]: + +[source, typescript, indent=0] +---- +import { + GraphQLInputInt, + GraphQLInputFloat, +} from 'graphql-input-number'; + +const argType = GraphQLInputInt({ + name: 'OneToNineInt', + min: 1, + max: 9, +}); + +new GraphQLObjectType({ + name: 'Query', + fields: { + input: { + type: GraphQLInt, + args: { + number: { + type: argType, + }, + }, + resolve: (_, {number}) => { + + // 'number' IS AN INT BETWEEN 1 to 9. + + }; + }, + }, +}); +---- + + +=== Rate-limit your API + +Rate-limiting an API means setting an upper bound to how many requests a client can send in a certain amount of time or how costly those requests may be. +There is more than one approach. +Several are outlined in the following sections. + + +==== Rate limit scores + +Refer to GitHub's link:https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-authenticated-users[Rate limits for the REST API]. + + +==== Query cost points + +The link:https://shopify.dev/docs/api/usage/limits#the-leaky-bucket-algorithm[leaky bucket algorithm]. + +==== Query cost analysis + +link:https://github.com/pa-bru/graphql-cost-analysis[raphql-cost-analysis] + + +=== Use timeouts + +To prevent the API from not responding or falling victim to denial of service attacks, it is feasible to make use of timeouts. +This way, subsequent queries will not be blocked by a long-running previous query. + +There are many ways and places to use timeouts. +Here are a few examples. + +// examples + + +=== Validate user input + +User input may potentially be malicious, for example, it could contain code snippets which get executed when running queries against the database. + +Follow the input validation methods summarized in the link:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#input-validation[OWASP Cheat Sheet Series]. + +// Examples? + + + +== Further reading + +Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. \ No newline at end of file From a8b9c7ff7c5395d01c44748898be01aec9e9e88e Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Mon, 22 Sep 2025 14:03:28 +0200 Subject: [PATCH 04/13] more text, draft --- modules/ROOT/content-nav.adoc | 1 + .../security/securing-a-graphql-api.adoc | 48 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 13c4af8e..d234b934 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -9,6 +9,7 @@ * *Reference* * xref:security/index.adoc[] +** xref:security/securing-a-graphql-api.adoc[] ** xref:security/configuration.adoc[] ** xref:security/authentication.adoc[] ** xref:security/authorization.adoc[] diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index a4544851..926d72c0 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -18,24 +18,54 @@ Lorem ipsum. === Authentication -Lorem ipsum. -Explanation on different auth options. +The xref:security/authentication.adoc[`@authentication` directive] can be applied globally, only to certain fields or only to certain types, and only for certain operations. -==== JWT +==== JSON Web Token (JWT) authentication -Lorem ipsum. +JWT authentication is a popular method for token-based authentication. +It allows clients to obtain and use tokens to authenticate subsequent requests. + +JWT are represent encoded JSON data. +These data can have arbitrary fields - which ones they should contain depends on the application preferences. +For instance, if the server side is trying to parse a field `roles_INCLUDES`, then the JWT should contain that. + +[source, graphql, indent=0] +---- +//Example +---- + +You can encode and decode JWT using a site like link:https://www.jwt.io/[https://www.jwt.io/]. -==== Lorem? +//==== Other methods? -ipsum. +//Lorem ipsum. === Authorization -Validate versus filter. -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. +The xref:security/authorization.adoc[`@authorization` directive] can either be used to filter out data which users should not have access to or throw an error if a query is executed against such data. + +Both have their own use cases: + +[source, graphql, indent=0] +---- +//Example for filtering +---- + +Lorem ipsum. + +[source, graphql, indent=0] +---- +//Example for validation +---- + +Lorem ipsum. + +It is important to be aware that error messages generated through validation can be a security concern since they can report database internals to your users. + +Also see <> on this page. == Best practice checklist @@ -50,7 +80,7 @@ While the xref:getting-started/graphql-aura.adoc[Getting started page for GraphQ Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious opLorem ipsumerations. Be sure to deactivate both in a customer-facing real-life scenario. - +[[best-practice-internal-errors]] === Catch internal errors For the same reason it is advisable to avoid introspection and data field suggestions, it can make your API more secure to catch internal errors and redact which information you want to pass on to the end user. From 9932cda2b010d025c5d72cf6964713d94783cb41 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Mon, 20 Oct 2025 16:25:09 +0200 Subject: [PATCH 05/13] WiP new type defs --- .../security/securing-a-graphql-api.adoc | 194 ++++++++++++++++-- 1 file changed, 182 insertions(+), 12 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index 926d72c0..bd2965b5 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -2,47 +2,149 @@ :description: This page is a tutorial on how to secure your API created with the Neo4j GraphQL Library. = Securing your GraphQL API -Lorem ipsum. +This page is a tutorial on how to secure your API created with the Neo4j GraphQL Library. == Prerequisites -Set up a new AuraDB instance. +. Set up a new AuraDB instance. Refer to link:https://neo4j.com/docs/aura/getting-started/create-instance/[Creating a Neo4j Aura instance]. +. Populate the instance with the Northwind data set. + +[NOTE] +==== +If you have completed the GraphQL and Aura Console getting started guide and would like to get rid of the example nodes you have created there, run the following in **Query** before populating your data base with the Northwind set: +[source,cypher] +---- +MATCH (n) DETACH DELETE n; +---- +==== +This tutorial is built on top of the xref:northwind-api.adoc[Northwind API tutorial]. +Specifically, it will extend the following type definitions: + +[source, graphql, indent=0] +---- +type Customer @node { + contactName: String! + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} +type Order @node { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") +} +type Product @node { + productName: String! + category: [Category!]! @relationship(type: "PART_OF", direction: OUT) + orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") + supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) +} +type Category @node { + categoryName: String! + products: [Product!]! @relationship(type: "PART_OF", direction: IN) +} +type Supplier @node { + supplierID: ID! @id + companyName: String! + products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) +} +type ordersProperties @relationshipProperties { + unitPrice: Float! + quantity: Int! +} +---- == Security-related directives -Lorem ipsum. +The GraphQL Library has several directives dedicated to security: `@authentication` and `@authorization`, as well as `@jwt` and `@jwtClaim`. === Authentication The xref:security/authentication.adoc[`@authentication` directive] can be applied globally, only to certain fields or only to certain types, and only for certain operations. +Add admin authorization for operations on customers, orders, products, categories and suppliers: + +* `DELETE` for customers, +* `UPDATE` and `DELETE` for orders, +* `CREATE`, `UPDATE` and `DELETE` for products, categories and suppliers. + +[source, graphql, indent=0] +---- +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + sensitiveData: String! @selectable(onRead: false, onAggregate: false) + createdAt: DateTime! @settable(onCreate: true, onUpdate: false) + adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} + +type Order + @node + @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") +} + +type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + productName: String! + category: [Category!]! @relationship(type: "PART_OF", direction: OUT) + orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") + supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) +} + +type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + categoryName: String! + products: [Product!]! @relationship(type: "PART_OF", direction: IN) +} + +type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + supplierID: ID! @id + companyName: String! + products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) +} +---- ==== JSON Web Token (JWT) authentication JWT authentication is a popular method for token-based authentication. It allows clients to obtain and use tokens to authenticate subsequent requests. -JWT are represent encoded JSON data. +JWT are represented by encoded JSON data. These data can have arbitrary fields - which ones they should contain depends on the application preferences. -For instance, if the server side is trying to parse a field `roles_INCLUDES`, then the JWT should contain that. +For instance, if the server side is trying to parse a field `roles`, then the JWT should contain that. +With `@jwtClaim`, you can specify a path to a customer ID in a nested location. +Here is an example: [source, graphql, indent=0] ---- -//Example +type JWT @jwt { + roles: [String!]! + customerID: String! @jwtClaim(path: "sub") +} ---- You can encode and decode JWT using a site like link:https://www.jwt.io/[https://www.jwt.io/]. -//==== Other methods? - -//Lorem ipsum. - - === Authorization The xref:security/authorization.adoc[`@authorization` directive] can either be used to filter out data which users should not have access to or throw an error if a query is executed against such data. @@ -258,4 +360,72 @@ Follow the input validation methods summarized in the link:https://cheatsheetser == Further reading -Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. \ No newline at end of file +Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. + + + + + + + +[source, graphql, indent=0] +---- + type JWT @jwt { + roles: [String!]! + customerID: String! @jwtClaim(path: "sub") + } + + type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + sensitiveData: String! @selectable(onRead: false, onAggregate: false) + createdAt: DateTime! @settable(onCreate: true, onUpdate: false) + adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) + } + + type Order + @node + @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") + } + + type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + productName: String! + category: [Category!]! @relationship(type: "PART_OF", direction: OUT) + orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") + supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) + } + + type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + categoryName: String! + products: [Product!]! @relationship(type: "PART_OF", direction: IN) + } + + type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + supplierID: ID! @id + companyName: String! + products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) + } + + type ordersProperties @relationshipProperties { + unitPrice: Float! + quantity: Int! + } +---- \ No newline at end of file From 671cbec97a88cf19646bfa78da02ceb5730b66c9 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Tue, 21 Oct 2025 15:37:11 +0200 Subject: [PATCH 06/13] some updates for sections --- .../security/securing-a-graphql-api.adoc | 246 ++++++++++++------ 1 file changed, 162 insertions(+), 84 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index bd2965b5..e14dfc57 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -20,8 +20,8 @@ MATCH (n) DETACH DELETE n; ---- ==== -This tutorial is built on top of the xref:northwind-api.adoc[Northwind API tutorial]. -Specifically, it will extend the following type definitions: +This tutorial builds on top of the xref:northwind-api.adoc[Northwind API tutorial]. +Specifically, it extends the following type definitions: [source, graphql, indent=0] ---- @@ -59,6 +59,7 @@ type ordersProperties @relationshipProperties { == Security-related directives The GraphQL Library has several directives dedicated to security: `@authentication` and `@authorization`, as well as `@jwt` and `@jwtClaim`. +The `@selectable` and `@settable` directives can be used to control accessibility of data fields through certain operations. === Authentication @@ -75,48 +76,54 @@ Add admin authorization for operations on customers, orders, products, categorie ---- type Customer @node - @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] + @authentication( <1> + operations: [DELETE], + jwt: { roles: { includes: "admin" } } ) { contactName: String! - sensitiveData: String! @selectable(onRead: false, onAggregate: false) - createdAt: DateTime! @settable(onCreate: true, onUpdate: false) - adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) customerID: ID! @id orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) } type Order @node - @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] - ) { + @authentication( <2> + operations: [UPDATE, DELETE], + jwt: { roles: { includes: "admin" } } + ) { orderID: ID! @id customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") } -type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { +type Product + @node + @authentication( <3> + operations: [CREATE, UPDATE, DELETE], + jwt: { roles: { includes: "admin" } } + ) { productName: String! category: [Category!]! @relationship(type: "PART_OF", direction: OUT) orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) } -type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { +type Category + @node + @authentication( <4> + operations: [CREATE, UPDATE, DELETE], + jwt: { roles: { includes: "admin" } } + ) { categoryName: String! products: [Product!]! @relationship(type: "PART_OF", direction: IN) } -type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { +type Supplier + @node + @authentication( <5> + operations: [CREATE, UPDATE, DELETE], + jwt: { roles: { includes: "admin" } } + ) { supplierID: ID! @id companyName: String! products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) @@ -130,9 +137,10 @@ It allows clients to obtain and use tokens to authenticate subsequent requests. JWT are represented by encoded JSON data. These data can have arbitrary fields - which ones they should contain depends on the application preferences. -For instance, if the server side is trying to parse a field `roles`, then the JWT should contain that. + +For instance, if the server side is trying to parse the `roles` field that was introduced in xref:#_authentication[], then the JWT should contain that. With `@jwtClaim`, you can specify a path to a customer ID in a nested location. -Here is an example: +For example: [source, graphql, indent=0] ---- @@ -149,27 +157,95 @@ You can encode and decode JWT using a site like link:https://www.jwt.io/[https:/ The xref:security/authorization.adoc[`@authorization` directive] can either be used to filter out data which users should not have access to or throw an error if a query is executed against such data. -Both have their own use cases: +Both have their own use cases. + +To make customer data and order data inaccessible to anyone who is not the specific user or an admin, consider the following uses of filters with the `@authorization` directive: [source, graphql, indent=0] ---- -//Example for filtering +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ <1> + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} + +type Order + @node + @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ <2> + { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") +} ---- -Lorem ipsum. +For sensitive data, you can also use a validating authorization: [source, graphql, indent=0] ---- -//Example for validation +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + adminNotes: [String!]! @authorization( + validate: [ <1> + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} ---- -Lorem ipsum. +`adminNotes` can only be read by admins and trying to access this field causes an error if the user is not an admin. It is important to be aware that error messages generated through validation can be a security concern since they can report database internals to your users. Also see <> on this page. +=== `@selectable` and `@settable` + +[source, graphql, indent=0] +---- +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + sensitiveData: String! @selectable(onRead: false, onAggregate: false) + createdAt: DateTime! @settable(onCreate: true, onUpdate: false) + adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} +---- + + == Best practice checklist Besides authentication and authorization considerations, there are a couple of worthwhile best practices to increase your API's security. @@ -358,74 +434,76 @@ Follow the input validation methods summarized in the link:https://cheatsheetser -== Further reading +== Full example -Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. +Here is the full set of type definitions extended with security-related directives: +[source, graphql, indent=0] +---- +type JWT @jwt { + roles: [String!]! + customerID: String! @jwtClaim(path: "sub") +} +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + sensitiveData: String! @selectable(onRead: false, onAggregate: false) + createdAt: DateTime! @settable(onCreate: true, onUpdate: false) + adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} +type Order + @node + @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") +} +type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + productName: String! + category: [Category!]! @relationship(type: "PART_OF", direction: OUT) + orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") + supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) +} +type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + categoryName: String! + products: [Product!]! @relationship(type: "PART_OF", direction: IN) +} +type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + supplierID: ID! @id + companyName: String! + products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) +} -[source, graphql, indent=0] +type ordersProperties @relationshipProperties { + unitPrice: Float! + quantity: Int! +} ---- - type JWT @jwt { - roles: [String!]! - customerID: String! @jwtClaim(path: "sub") - } - type Customer - @node - @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] - ) { - contactName: String! - sensitiveData: String! @selectable(onRead: false, onAggregate: false) - createdAt: DateTime! @settable(onCreate: true, onUpdate: false) - adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) - customerID: ID! @id - orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) - } - type Order - @node - @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] - ) { - orderID: ID! @id - customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) - products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") - } +== Further reading - type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - productName: String! - category: [Category!]! @relationship(type: "PART_OF", direction: OUT) - orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") - supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) - } +Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. - type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - categoryName: String! - products: [Product!]! @relationship(type: "PART_OF", direction: IN) - } - type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - supplierID: ID! @id - companyName: String! - products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) - } - type ordersProperties @relationshipProperties { - unitPrice: Float! - quantity: Int! - } ----- \ No newline at end of file From 6891e80cb04d054431860d595721f661b82dd489 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Fri, 24 Oct 2025 10:21:54 +0200 Subject: [PATCH 07/13] adjusted structure, explained directives --- .../security/securing-a-graphql-api.adoc | 143 +++++++++--------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index e14dfc57..fac3e4dd 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -58,8 +58,8 @@ type ordersProperties @relationshipProperties { == Security-related directives -The GraphQL Library has several directives dedicated to security: `@authentication` and `@authorization`, as well as `@jwt` and `@jwtClaim`. -The `@selectable` and `@settable` directives can be used to control accessibility of data fields through certain operations. +The GraphQL Library has several directives dedicated to security: xref:security/authentication.adoc[`@authentication`] and xref:security/authorization.adoc[`@authorization`], as well as `@jwt` and `@jwtClaim`. +The xref:directives/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] and xref:directives/schema-configuration/field-configuration.adoc#_settable[`@settable`] directives can be used to control accessibility of data fields through certain operations. === Authentication @@ -225,8 +225,43 @@ Also see <> on this page. === `@selectable` and `@settable` +To restrict access through operations directly, you can use the xref:directives/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] and xref:directives/schema-configuration/field-configuration.adoc#_settable[`@settable`] directives, for example: + +[source, graphql, indent=0] +---- +type Customer + @node + @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + contactName: String! + sensitiveData: String! @selectable(onRead: false, onAggregate: false) + createdAt: DateTime! @settable(onCreate: true, onUpdate: false) + adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) + customerID: ID! @id + orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) +} +---- + +The `sensitiveData` field is neither available for queries nor for subscriptions nor for aggregations. +The `createdAt` field can be set when a new customer is created, but it cannot be updated. + + +=== Full example + +Here is the full set of type definitions extended with security-related directives: + [source, graphql, indent=0] ---- +type JWT @jwt { + roles: [String!]! + customerID: String! @jwtClaim(path: "sub") +} + type Customer @node @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) @@ -243,6 +278,43 @@ type Customer customerID: ID! @id orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) } + +type Order + @node + @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) + @authorization( + filter: [ + { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } + { where: { jwt: { roles: { includes: "admin" } } } } + ] + ) { + orderID: ID! @id + customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) + products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") +} + +type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + productName: String! + category: [Category!]! @relationship(type: "PART_OF", direction: OUT) + orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") + supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) +} + +type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + categoryName: String! + products: [Product!]! @relationship(type: "PART_OF", direction: IN) +} + +type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { + supplierID: ID! @id + companyName: String! + products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) +} + +type ordersProperties @relationshipProperties { + unitPrice: Float! + quantity: Int! +} ---- @@ -434,73 +506,6 @@ Follow the input validation methods summarized in the link:https://cheatsheetser -== Full example - -Here is the full set of type definitions extended with security-related directives: - -[source, graphql, indent=0] ----- -type JWT @jwt { - roles: [String!]! - customerID: String! @jwtClaim(path: "sub") -} - -type Customer - @node - @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] - ) { - contactName: String! - sensitiveData: String! @selectable(onRead: false, onAggregate: false) - createdAt: DateTime! @settable(onCreate: true, onUpdate: false) - adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }]) - customerID: ID! @id - orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT) -} - -type Order - @node - @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) - @authorization( - filter: [ - { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } - { where: { jwt: { roles: { includes: "admin" } } } } - ] - ) { - orderID: ID! @id - customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN) - products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties") -} - -type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - productName: String! - category: [Category!]! @relationship(type: "PART_OF", direction: OUT) - orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties") - supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN) -} - -type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - categoryName: String! - products: [Product!]! @relationship(type: "PART_OF", direction: IN) -} - -type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) { - supplierID: ID! @id - companyName: String! - products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT) -} - -type ordersProperties @relationshipProperties { - unitPrice: Float! - quantity: Int! -} ----- - - == Further reading Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. From 13d26cecc8a7da63a70bea7fcd4bc7b483ceec99 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Fri, 24 Oct 2025 10:26:00 +0200 Subject: [PATCH 08/13] added two xrefs --- modules/ROOT/pages/security/securing-a-graphql-api.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index fac3e4dd..cc0d3629 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -58,7 +58,7 @@ type ordersProperties @relationshipProperties { == Security-related directives -The GraphQL Library has several directives dedicated to security: xref:security/authentication.adoc[`@authentication`] and xref:security/authorization.adoc[`@authorization`], as well as `@jwt` and `@jwtClaim`. +The GraphQL Library has several directives dedicated to security: xref:security/authentication.adoc[`@authentication`] and xref:security/authorization.adoc[`@authorization`], as well as xref:security/configuration.adoc#_jwt[`@jwt`] and xref:security/configuration.adoc#_jwtclaim[`@jwtClaim`]. The xref:directives/schema-configuration/field-configuration.adoc#_selectable[`@selectable`] and xref:directives/schema-configuration/field-configuration.adoc#_settable[`@settable`] directives can be used to control accessibility of data fields through certain operations. From e6743bd94fc515e149c795fabb33f314d7838ebc Mon Sep 17 00:00:00 2001 From: Richard Sill <156673635+rsill-neo4j@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:29:25 +0100 Subject: [PATCH 09/13] Update modules/ROOT/pages/security/securing-a-graphql-api.adoc --- modules/ROOT/pages/security/securing-a-graphql-api.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index cc0d3629..8d847c69 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -20,7 +20,7 @@ MATCH (n) DETACH DELETE n; ---- ==== -This tutorial builds on top of the xref:northwind-api.adoc[Northwind API tutorial]. +This tutorial builds on top of the xref:graphql-modeling.adoc[GraphQL modeling tutorial]. Specifically, it extends the following type definitions: [source, graphql, indent=0] From 4bd172a94f1741152d633f315f76723f91f5a52d Mon Sep 17 00:00:00 2001 From: Richard Sill <156673635+rsill-neo4j@users.noreply.github.com> Date: Mon, 10 Nov 2025 16:51:07 +0100 Subject: [PATCH 10/13] Apply suggestions from code review Co-authored-by: kerem --- modules/ROOT/pages/security/securing-a-graphql-api.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index 8d847c69..dd89d436 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -327,7 +327,7 @@ Besides authentication and authorization considerations, there are a couple of w While the xref:getting-started/graphql-aura.adoc[Getting started page for GraphQL and Aura Console] advocates to both **Enable introspection** as well as **Enable field suggestions**, this is not recommended when considering security. -Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious opLorem ipsumerations. +Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious operations. Be sure to deactivate both in a customer-facing real-life scenario. [[best-practice-internal-errors]] @@ -482,7 +482,7 @@ The link:https://shopify.dev/docs/api/usage/limits#the-leaky-bucket-algorithm[le ==== Query cost analysis -link:https://github.com/pa-bru/graphql-cost-analysis[raphql-cost-analysis] +link:https://github.com/pa-bru/graphql-cost-analysis[graphql-cost-analysis] === Use timeouts From aa4fbf9a5034afbc41e754da729fd0cd838c3aa8 Mon Sep 17 00:00:00 2001 From: Richard Sill <156673635+rsill-neo4j@users.noreply.github.com> Date: Thu, 20 Nov 2025 17:36:30 +0100 Subject: [PATCH 11/13] Apply suggestions from code review --- .../security/securing-a-graphql-api.adoc | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index dd89d436..3a0280ab 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -330,34 +330,6 @@ While the xref:getting-started/graphql-aura.adoc[Getting started page for GraphQ Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious operations. Be sure to deactivate both in a customer-facing real-life scenario. -[[best-practice-internal-errors]] -=== Catch internal errors - -For the same reason it is advisable to avoid introspection and data field suggestions, it can make your API more secure to catch internal errors and redact which information you want to pass on to the end user. - -For example, the following error reveals information XY: - -[source, json, indent=0] ----- -"data": { - "field": "value" -} ----- - -You can use Apollo Server's link:https://www.apollographql.com/docs/apollo-server/data/errors[Error Handling] to catch such internal errors and then decide how to pass this on to your users: - -[source, typescript, indent=0] ----- -import { ApolloServerErrorCode } from '@apollo/server/errors'; - -if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED) { - // respond to the syntax error - -} else if (error.extensions?.code === "MY_CUSTOM_CODE") { - // do something else - -} ----- === Limit query depth @@ -496,15 +468,6 @@ Here are a few examples. // examples -=== Validate user input - -User input may potentially be malicious, for example, it could contain code snippets which get executed when running queries against the database. - -Follow the input validation methods summarized in the link:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html#input-validation[OWASP Cheat Sheet Series]. - -// Examples? - - == Further reading From 174b276c1c5e8247e467c706bc932f199a7a47ed Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Thu, 4 Dec 2025 17:36:21 +0100 Subject: [PATCH 12/13] updates --- .../security/securing-a-graphql-api.adoc | 129 ++++++++++-------- 1 file changed, 69 insertions(+), 60 deletions(-) diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index 3a0280ab..ce47f9f6 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -357,21 +357,40 @@ query { } ---- -This can be achieved with a package such as link:https://www.npmjs.com/package/graphql-depth-limit[graphql-depth-limit]: +This can be achieved with link:https://escape.tech/graphql-armor/docs/plugins/max-depth/[GraphQL Armor]: [source, typescript, indent=0] ---- -import depthLimit from 'graphql-depth-limit' -import express from 'express' -import graphqlHTTP from 'express-graphql' -import schema from './schema' - -const app = express() - -app.use('/graphql', graphqlHTTP((req, res) => ({ - schema, - validationRules: [ depthLimit(10) ] -}))) +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { ApolloArmor } from '@escape.tech/graphql-armor'; +import { readFileSync } from 'fs'; + +// Assume you have your schema definition in a string or file. +const typeDefs = readFileSync('./your-schema.graphql', 'utf-8'); +const resolvers = { /* Your resolvers here. */ }; +// Instantiate GraphQL Armor and configure the maxDepth plugin. +const armor = new ApolloArmor({ + maxDepth: { + enabled: true, + n: 5, // Sets the maximum allowed query depth to 5. + }, +}); + +// Get the security plugins provided by Armor. +const plugins = armor.protect(); + +const server = new ApolloServer({ + typeDefs, + resolvers, + plugins: [...plugins], // Add the armor plugins to Apollo Server. +}); + +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, +}); + +console.log(`πŸš€ Server ready at ${url}`); ---- @@ -393,46 +412,45 @@ query { } ---- -To avoid this, you can cap the input number directly in your resolvers, for example like this: +You can prevent denial of service attacks based on queries such as this by paginating query results. + +A server-side pagination solution based on type definitions could look like this: [source, graphql, indent=0] ---- -// example ----- +type PageInfo { + startCursor: String + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! +} -Alternatively, use a library such as link:https://github.com/joonhocho/graphql-input-number[graphql-input-number]: +// Connection types for nested data (Products within an Order) +type ProductEdge { + node: Product! + cursor: String! +} -[source, typescript, indent=0] ----- -import { - GraphQLInputInt, - GraphQLInputFloat, -} from 'graphql-input-number'; - -const argType = GraphQLInputInt({ - name: 'OneToNineInt', - min: 1, - max: 9, -}); +type ProductConnection { + edges: [ProductEdge!]! + pageInfo: PageInfo! +} -new GraphQLObjectType({ - name: 'Query', - fields: { - input: { - type: GraphQLInt, - args: { - number: { - type: argType, - }, - }, - resolve: (_, {number}) => { - - // 'number' IS AN INT BETWEEN 1 to 9. - - }; - }, - }, -}); +// Connection types for root-level data (Orders list) +type OrderEdge { + node: Order! + cursor: String! +} + +type OrderConnection { + edges: [OrderEdge!]! + pageInfo: PageInfo! +} + +// The root query that is targeted +type Query { + orders(first: Int, after: String, last: Int, before: String): OrderConnection +} ---- @@ -443,30 +461,21 @@ There is more than one approach. Several are outlined in the following sections. -==== Rate limit scores - -Refer to GitHub's link:https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-authenticated-users[Rate limits for the REST API]. - - ==== Query cost points -The link:https://shopify.dev/docs/api/usage/limits#the-leaky-bucket-algorithm[leaky bucket algorithm]. +The link:https://shopify.dev/docs/api/usage/limits#the-leaky-bucket-algorithm[leaky bucket algorithm] represents an algorithmic solution to slow down the processing of multiple requests at once. -==== Query cost analysis -link:https://github.com/pa-bru/graphql-cost-analysis[graphql-cost-analysis] +==== Query cost analysis +link:https://escape.tech/graphql-armor/docs/plugins/cost-limit/[GraphQL Armor] offers a way to limit the cost of GraphQL queries. === Use timeouts To prevent the API from not responding or falling victim to denial of service attacks, it is feasible to make use of timeouts. -This way, subsequent queries will not be blocked by a long-running previous query. - -There are many ways and places to use timeouts. -Here are a few examples. - -// examples +This way, subsequent queries aren't blocked by a long-running previous query. +You can set a timeout via the GraphQL Library driver, see xref:driver-configuration.adoc#_transaction_configuration_in_context[Transaction configuration in context]. == Further reading From 351759ade2878419db31596796c2b8f152b96044 Mon Sep 17 00:00:00 2001 From: Richard Sill Date: Tue, 16 Dec 2025 15:55:59 +0100 Subject: [PATCH 13/13] review suggestions --- modules/ROOT/content-nav.adoc | 2 +- .../security/securing-a-graphql-api.adoc | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/ROOT/content-nav.adoc b/modules/ROOT/content-nav.adoc index 16e6b20f..26997ed5 100644 --- a/modules/ROOT/content-nav.adoc +++ b/modules/ROOT/content-nav.adoc @@ -10,7 +10,6 @@ * *Reference* * xref:security/index.adoc[] -** xref:security/securing-a-graphql-api.adoc[] ** xref:security/configuration.adoc[] ** xref:security/authentication.adoc[] ** xref:security/authorization.adoc[] @@ -58,6 +57,7 @@ * xref:driver-configuration.adoc[] * xref:graphql-modeling.adoc[] +* xref:security/securing-a-graphql-api.adoc[] * *Products* diff --git a/modules/ROOT/pages/security/securing-a-graphql-api.adoc b/modules/ROOT/pages/security/securing-a-graphql-api.adoc index ce47f9f6..130cbf82 100644 --- a/modules/ROOT/pages/security/securing-a-graphql-api.adoc +++ b/modules/ROOT/pages/security/securing-a-graphql-api.adoc @@ -64,9 +64,9 @@ The xref:directives/schema-configuration/field-configuration.adoc#_selectable[`@ === Authentication -The xref:security/authentication.adoc[`@authentication` directive] can be applied globally, only to certain fields or only to certain types, and only for certain operations. +You can apply the xref:security/authentication.adoc[`@authentication` directive] either globally, only to certain fields or only to certain types, and only for certain operations. -Add admin authorization for operations on customers, orders, products, categories and suppliers: +Add authentication as an admin to operations on customers, orders, products, categories and suppliers: * `DELETE` for customers, * `UPDATE` and `DELETE` for orders, @@ -139,7 +139,9 @@ JWT are represented by encoded JSON data. These data can have arbitrary fields - which ones they should contain depends on the application preferences. For instance, if the server side is trying to parse the `roles` field that was introduced in xref:#_authentication[], then the JWT should contain that. -With `@jwtClaim`, you can specify a path to a customer ID in a nested location. +Specify the types of JWT data with `@jwt`. +Then you can specify a path to a customer ID in a nested location with `@jwtClaim`. + For example: [source, graphql, indent=0] @@ -167,7 +169,7 @@ type Customer @node @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } }) @authorization( - filter: [ <1> + filter: [ { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } } { where: { jwt: { roles: { includes: "admin" } } } } ] @@ -181,7 +183,7 @@ type Order @node @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) @authorization( - filter: [ <2> + filter: [ { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } } { where: { jwt: { roles: { includes: "admin" } } } } ] @@ -207,7 +209,7 @@ type Customer ) { contactName: String! adminNotes: [String!]! @authorization( - validate: [ <1> + validate: [ { where: { jwt: { roles: { includes: "admin" } } } } ] ) @@ -323,13 +325,12 @@ type ordersProperties @relationshipProperties { Besides authentication and authorization considerations, there are a couple of worthwhile best practices to increase your API's security. -=== Avoid introspection and data field suggestions +=== Introspection and data field suggestions While the xref:getting-started/graphql-aura.adoc[Getting started page for GraphQL and Aura Console] advocates to both **Enable introspection** as well as **Enable field suggestions**, this is not recommended when considering security. Both potentially expose information that can be used to gain insight on specifics of your GraphQL schema and execute targeted malicious operations. -Be sure to deactivate both in a customer-facing real-life scenario. - +We recommend you to deactivate both in a customer-facing real-life scenario unless you have a good reason to use them. === Limit query depth @@ -482,5 +483,4 @@ You can set a timeout via the GraphQL Library driver, see xref:driver-configurat Neo4j has a link:https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/[Role-based access control] mechanism that can be leveraged to increase security even further. - - +For more security-related topics in GraphQL, refer to the link:https://www.graphql.org/learn/security/[GraphQL Security] page. \ No newline at end of file