Skip to content

Commit 671cbec

Browse files
committed
some updates for sections
1 parent 9932cda commit 671cbec

File tree

1 file changed

+162
-84
lines changed

1 file changed

+162
-84
lines changed

modules/ROOT/pages/security/securing-a-graphql-api.adoc

Lines changed: 162 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6161
The 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
----
7677
type 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
9388
type 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

131138
JWT are represented by encoded JSON data.
132139
These 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.
134142
With `@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

150158
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.
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

168221
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.
169222

170223
Also 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

175251
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
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

Comments
 (0)