@@ -20,8 +20,8 @@ MATCH (n) DETACH DELETE n;
2020----
2121====
2222
23- This tutorial is built on top of the xref:northwind-api.adoc[Northwind API tutorial].
24- Specifically, it will extend the following type definitions:
23+ This tutorial builds on top of the xref:northwind-api.adoc[Northwind API tutorial].
24+ Specifically, it extends the following type definitions:
2525
2626[source, graphql, indent=0]
2727----
@@ -59,6 +59,7 @@ type ordersProperties @relationshipProperties {
5959== Security-related directives
6060
6161The GraphQL Library has several directives dedicated to security: `@authentication` and `@authorization`, as well as `@jwt` and `@jwtClaim`.
62+ The `@selectable` and `@settable` directives can be used to control accessibility of data fields through certain operations.
6263
6364
6465=== Authentication
@@ -75,48 +76,54 @@ Add admin authorization for operations on customers, orders, products, categorie
7576----
7677type Customer
7778 @node
78- @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
79- @authorization(
80- filter: [
81- { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
82- { where: { jwt: { roles: { includes: "admin" } } } }
83- ]
79+ @authentication( <1>
80+ operations: [DELETE],
81+ jwt: { roles: { includes: "admin" } }
8482 ) {
8583 contactName: String!
86- sensitiveData: String! @selectable(onRead: false, onAggregate: false)
87- createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
88- adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
8984 customerID: ID! @id
9085 orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
9186}
9287
9388type Order
9489 @node
95- @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
96- @authorization(
97- filter: [
98- { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
99- { where: { jwt: { roles: { includes: "admin" } } } }
100- ]
101- ) {
90+ @authentication( <2>
91+ operations: [UPDATE, DELETE],
92+ jwt: { roles: { includes: "admin" } }
93+ ) {
10294 orderID: ID! @id
10395 customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
10496 products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
10597}
10698
107- type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
99+ type Product
100+ @node
101+ @authentication( <3>
102+ operations: [CREATE, UPDATE, DELETE],
103+ jwt: { roles: { includes: "admin" } }
104+ ) {
108105 productName: String!
109106 category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
110107 orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
111108 supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
112109}
113110
114- type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
111+ type Category
112+ @node
113+ @authentication( <4>
114+ operations: [CREATE, UPDATE, DELETE],
115+ jwt: { roles: { includes: "admin" } }
116+ ) {
115117 categoryName: String!
116118 products: [Product!]! @relationship(type: "PART_OF", direction: IN)
117119}
118120
119- type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
121+ type Supplier
122+ @node
123+ @authentication( <5>
124+ operations: [CREATE, UPDATE, DELETE],
125+ jwt: { roles: { includes: "admin" } }
126+ ) {
120127 supplierID: ID! @id
121128 companyName: String!
122129 products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
@@ -130,9 +137,10 @@ It allows clients to obtain and use tokens to authenticate subsequent requests.
130137
131138JWT are represented by encoded JSON data.
132139These data can have arbitrary fields - which ones they should contain depends on the application preferences.
133- For instance, if the server side is trying to parse a field `roles`, then the JWT should contain that.
140+
141+ 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.
134142With `@jwtClaim`, you can specify a path to a customer ID in a nested location.
135- Here is an example:
143+ For example:
136144
137145[source, graphql, indent=0]
138146----
@@ -149,27 +157,95 @@ You can encode and decode JWT using a site like link:https://www.jwt.io/[https:/
149157
150158The 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.
151159
152- Both have their own use cases:
160+ Both have their own use cases.
161+
162+ 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:
153163
154164[source, graphql, indent=0]
155165----
156- //Example for filtering
166+ type Customer
167+ @node
168+ @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
169+ @authorization(
170+ filter: [ <1>
171+ { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
172+ { where: { jwt: { roles: { includes: "admin" } } } }
173+ ]
174+ ) {
175+ contactName: String!
176+ customerID: ID! @id
177+ orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
178+ }
179+
180+ type Order
181+ @node
182+ @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
183+ @authorization(
184+ filter: [ <2>
185+ { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
186+ { where: { jwt: { roles: { includes: "admin" } } } }
187+ ]
188+ ) {
189+ orderID: ID! @id
190+ customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
191+ products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
192+ }
157193----
158194
159- Lorem ipsum.
195+ For sensitive data, you can also use a validating authorization:
160196
161197[source, graphql, indent=0]
162198----
163- //Example for validation
199+ type Customer
200+ @node
201+ @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
202+ @authorization(
203+ filter: [
204+ { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
205+ { where: { jwt: { roles: { includes: "admin" } } } }
206+ ]
207+ ) {
208+ contactName: String!
209+ adminNotes: [String!]! @authorization(
210+ validate: [ <1>
211+ { where: { jwt: { roles: { includes: "admin" } } } }
212+ ]
213+ )
214+ customerID: ID! @id
215+ orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
216+ }
164217----
165218
166- Lorem ipsum .
219+ `adminNotes` can only be read by admins and trying to access this field causes an error if the user is not an admin .
167220
168221It 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.
169222
170223Also see <<best-practice-internal-errors>> on this page.
171224
172225
226+ === `@selectable` and `@settable`
227+
228+ [source, graphql, indent=0]
229+ ----
230+ type Customer
231+ @node
232+ @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
233+ @authorization(
234+ filter: [
235+ { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
236+ { where: { jwt: { roles: { includes: "admin" } } } }
237+ ]
238+ ) {
239+ contactName: String!
240+ sensitiveData: String! @selectable(onRead: false, onAggregate: false)
241+ createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
242+ adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
243+ customerID: ID! @id
244+ orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
245+ }
246+ ----
247+
248+
173249== Best practice checklist
174250
175251Besides 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
358434
359435
360436
361- == Further reading
437+ == Full example
362438
363- 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.
439+ Here is the full set of type definitions extended with security-related directives:
364440
441+ [source, graphql, indent=0]
442+ ----
443+ type JWT @jwt {
444+ roles: [String!]!
445+ customerID: String! @jwtClaim(path: "sub")
446+ }
365447
448+ type Customer
449+ @node
450+ @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
451+ @authorization(
452+ filter: [
453+ { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
454+ { where: { jwt: { roles: { includes: "admin" } } } }
455+ ]
456+ ) {
457+ contactName: String!
458+ sensitiveData: String! @selectable(onRead: false, onAggregate: false)
459+ createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
460+ adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
461+ customerID: ID! @id
462+ orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
463+ }
366464
465+ type Order
466+ @node
467+ @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
468+ @authorization(
469+ filter: [
470+ { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
471+ { where: { jwt: { roles: { includes: "admin" } } } }
472+ ]
473+ ) {
474+ orderID: ID! @id
475+ customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
476+ products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
477+ }
367478
479+ type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
480+ productName: String!
481+ category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
482+ orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
483+ supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
484+ }
368485
486+ type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
487+ categoryName: String!
488+ products: [Product!]! @relationship(type: "PART_OF", direction: IN)
489+ }
369490
491+ type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
492+ supplierID: ID! @id
493+ companyName: String!
494+ products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
495+ }
370496
371- [source, graphql, indent=0]
497+ type ordersProperties @relationshipProperties {
498+ unitPrice: Float!
499+ quantity: Int!
500+ }
372501----
373- type JWT @jwt {
374- roles: [String!]!
375- customerID: String! @jwtClaim(path: "sub")
376- }
377502
378- type Customer
379- @node
380- @authentication(operations: [DELETE], jwt: { roles: { includes: "admin" } })
381- @authorization(
382- filter: [
383- { operations: [READ], where: { node: { customerID: { eq: "$jwt.customerID" } } } }
384- { where: { jwt: { roles: { includes: "admin" } } } }
385- ]
386- ) {
387- contactName: String!
388- sensitiveData: String! @selectable(onRead: false, onAggregate: false)
389- createdAt: DateTime! @settable(onCreate: true, onUpdate: false)
390- adminNotes: [String!]! @authorization(validate: [{ where: { jwt: { roles: { includes: "admin" } } } }])
391- customerID: ID! @id
392- orders: [Order!]! @relationship(type: "PURCHASED", direction: OUT)
393- }
394503
395- type Order
396- @node
397- @authentication(operations: [UPDATE, DELETE], jwt: { roles: { includes: "admin" } })
398- @authorization(
399- filter: [
400- { where: { node: { customer: { all: { customerID: { eq: "$jwt.customerID" } } } } } }
401- { where: { jwt: { roles: { includes: "admin" } } } }
402- ]
403- ) {
404- orderID: ID! @id
405- customer: [Customer!]! @relationship(type: "PURCHASED", direction: IN)
406- products: [Product!]! @relationship(type: "ORDERS", direction: OUT, properties: "ordersProperties")
407- }
504+ == Further reading
408505
409- type Product @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
410- productName: String!
411- category: [Category!]! @relationship(type: "PART_OF", direction: OUT)
412- orders: [Product!]! @relationship(type: "ORDERS", direction: IN, properties: "ordersProperties")
413- supplier: [Supplier!]! @relationship(type: "SUPPLIES", direction: IN)
414- }
506+ 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.
415507
416- type Category @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
417- categoryName: String!
418- products: [Product!]! @relationship(type: "PART_OF", direction: IN)
419- }
420508
421- type Supplier @node @authentication(operations: [CREATE, UPDATE, DELETE], jwt: { roles: { includes: "admin" } }) {
422- supplierID: ID! @id
423- companyName: String!
424- products: [Product!]! @relationship(type: "SUPPLIES", direction: OUT)
425- }
426509
427- type ordersProperties @relationshipProperties {
428- unitPrice: Float!
429- quantity: Int!
430- }
431- ----
0 commit comments