diff --git a/modules/ROOT/pages/directives/index.adoc b/modules/ROOT/pages/directives/index.adoc index a8929ad3..8d30fa4a 100644 --- a/modules/ROOT/pages/directives/index.adoc +++ b/modules/ROOT/pages/directives/index.adoc @@ -40,10 +40,10 @@ a| Required to differentiate interfaces that are used for relationship propertie | xref::/security/authorization.adoc[`@authorization`] | Specifies authorization rules for queries and mutations on the type. -| xref::/security/configuration.adoc#authentication-and-authorization-jwt[`@jwt`] +| xref::/security/configuration.adoc#_the_jwt_directive[`@jwt`] | Configures the JWT authentication and authorization filters to include additional JWT claims. -| xref::/security/configuration.adoc#_nested_claims[`@jwtClaim`] +| xref::/security/configuration.adoc#_the_jwtclaim_directive[`@jwtClaim`] | Used in combination with `@jwt`. Configures the JWT authentication and authorization filters to include an additional JWT claim which is either nested or using special characters not supported by GraphQL. diff --git a/modules/ROOT/pages/security/authentication.adoc b/modules/ROOT/pages/security/authentication.adoc index 3ddedfc7..b8a089a8 100644 --- a/modules/ROOT/pages/security/authentication.adoc +++ b/modules/ROOT/pages/security/authentication.adoc @@ -1,13 +1,67 @@ = Authentication :description: This page describes how to set up authentication features in the Neo4j GraphQL Library. -Explicit authentication, configured using the `@authentication` directive, is only ever evaluated -during Cypher translation time, and unauthenticated requests with queries requiring authentication -will never reach the database. +The GraphQL Library offers the `@authentication` directive to configure authentication for certain operations and for different parts of your schema. -== Configuration +[IMPORTANT] +==== +Explicit authentication, configured with the `@authentication` directive, is only ever evaluated during Cypher translation time. +Unauthenticated requests with queries requiring authentication never reach the database. +==== -Authentication can be configured for an entire type, for example, the type `User`: +== Operations + +Authentication can be configured to only be validated on certain operations: + +* `CREATE` +* `READ` +* `AGGREGATE` +* `UPDATE` +* `DELETE` +* `CREATE_RELATIONSHIP` +* `DELETE_RELATIONSHIP` +* `SUBSCRIBE` + +For instance, to only require authentication for the update or deletion of a user: + +[source, graphql, indent=0] +---- +type User @authentication(operations: [UPDATE, DELETE]) { + id: ID! + name: String! + password: String! +} +---- + +[NOTE] +==== +In case there is no `operations` argument with a list of operations, the GraphQL Library treats the authentication configuration as if the full list of operations had been provided. +==== + +== Scope + +=== Global authentication + +Authentication can be applied to the entire schema. +This ensures authentication is checked for every matching request. + +Extend the schema: + +[source, graphql, indent=0] +---- +extend schema @authentication +---- + +The `operations` and `jwt` arguments can also be used when the directive is applied to a schema extension, for example: + +[source, graphql, indent=0] +---- +extend schema @authentication(operations: [UPDATE, DELETE], jwt: { roles_INCLUDES: "admin" }) +---- + +=== Authentication for types + +Authentication can be configured for an entire type: [source, graphql, indent=0] ---- @@ -18,7 +72,7 @@ type User @authentication { } ---- -Authentication will thus be validated when any of the following operations are _attempted_: +With this configuration, authentication is validated when any of the following operations are _attempted_: * *Create*: `createUsers` mutation, `create`, or `connectOrCreate` nested operation via a related type. * *Read*: `users`, `usersConnection`, `usersAggregate` query, or access via related type. @@ -28,7 +82,10 @@ Authentication will thus be validated when any of the following operations are _ * *Delete relationship*: `disconnect` nested operation via a related type. * *Subscribe*: all subscription operations related to type `User`. -Additionally, the directive can be configured on a per-field basis, for example: + +=== Authentication for fields + +Authentication can be configured on a per-field basis, for example: [source, graphql, indent=0] ---- @@ -39,37 +96,13 @@ type User { } ---- -This will only be evaluated in the following circumstances: +This is only evaluated under the following circumstances: * The `password` field is set on either `create` or `update`. * The `password` field is present in a selection set. -=== Operations - -Authentication can be configured to only be validated on certain operations: - -* `CREATE` -* `READ` -* `AGGREGATE` -* `UPDATE` -* `DELETE` -* `CREATE_RELATIONSHIP` -* `DELETE_RELATIONSHIP` -* `SUBSCRIBE` - - -For instance, to only require authentication for the update or deletion of a user: - -[source, graphql, indent=0] ----- -type User @authentication(operations: [UPDATE, DELETE]) { - id: ID! - name: String! - password: String! -} ----- -=== Additional verification +== Additional verification Additional checks against JWT claims can be performed together with authentication. For instance, if it was a requirement that only users with the `admin` role can delete users: @@ -81,18 +114,4 @@ type User @authentication(operations: [DELETE], jwt: { roles_INCLUDES: "admin" } name: String! password: String! } ----- - -== Global authentication - -Additionally, authentication can be applied to the entire schema. -This ensures authentication is checked for every matching request. - -This is done via extending the schema: - -[source, graphql, indent=0] ----- -extend schema @authentication ----- - -The `operations` and `jwt` arguments can also be used when the directive is applied to a schema extension. +---- \ No newline at end of file diff --git a/modules/ROOT/pages/security/authorization.adoc b/modules/ROOT/pages/security/authorization.adoc index adb5aa1b..39a3f5f0 100644 --- a/modules/ROOT/pages/security/authorization.adoc +++ b/modules/ROOT/pages/security/authorization.adoc @@ -66,6 +66,11 @@ type Post @authorization(filter: [ } ---- +[NOTE] +==== +In case there is no `operations` argument with a list of operations, the GraphQL Library treats the authorization configuration as if the full list of operations had been provided. +==== + === Validating @@ -115,6 +120,11 @@ type Post @authorization(validate: [ } ---- +[NOTE] +==== +In case there is no `operations` argument with a list of operations, the GraphQL Library treats the authorization configuration as if the full list of operations had been provided. +==== + == Authorization without authentication diff --git a/modules/ROOT/pages/security/configuration.adoc b/modules/ROOT/pages/security/configuration.adoc index c392d997..2660cee1 100644 --- a/modules/ROOT/pages/security/configuration.adoc +++ b/modules/ROOT/pages/security/configuration.adoc @@ -1,16 +1,25 @@ = Configuration :description: This page describes how to configure authentication and authorization features in the Neo4j GraphQL Library. +The Neo4j GraphQL Library uses JSON Web Token (JWT) authentication. +JWTs are tokens containing claims or statements about the user or client making the request. +These claims can include information such as the user's ID or roles. + +A JWT can be obtained from an authentication service and then be included in an API request. +The API verifies the JWT and returns the requested data if the JWT is valid. + == Instantiation -The Neo4j GraphQL Library can accept JSON Web Tokens via two mechanisms: +The Neo4j GraphQL Library can accept two types of JWTs: * Encoded JWTs in the `token` field of the request context. * Decoded JWTs in the `jwt` field of the request context. -If using the former, the library will need to be configured with a key to decode and verify the token. +=== Encoded JWTs -The following code block demonstrates using Apollo Server, extracting the `Authorization` header from the request, and putting it into the appropriate context field: +In order to use encoded JWTs, configure the library with a key to decode and verify the tokens. +The following code block uses Apollo Server. +It extracts the `Authorization` header from the request and puts it in the appropriate context field: [source, typescript, indent=0] ---- @@ -28,7 +37,9 @@ const { url } = await startStandaloneServer(server, { Optionally, if a custom decoding mechanism is required, that same header can be decoded and the resulting JWT payload put into the `jwt` field of the context. -=== Symmetric secret +Alternatively, you can decode a token via a xref:#_jwks_endpoint[]. + +==== Symmetric secret To configure the library with a symmetric secret (e.g. "secret"), the following instantiation is required: @@ -44,9 +55,9 @@ new Neo4jGraphQL({ }); ---- -=== JWKS endpoint +==== JWKS endpoint -To configure the library to verify tokens against a JWKS endpoint, "https://www.myapplication.com/.well-known/jwks.json", the following instantiation is required: +To configure the library to verify tokens against a JSON Web Key Set (JWKS) endpoint, for example "https://www.example.com/.well-known/jwks.json", the following instantiation is required: [source, typescript, indent=0] ---- @@ -62,14 +73,82 @@ new Neo4jGraphQL({ }); ---- -[[authentication-and-authorization-jwt]] -== JWT +==== Passing in encoded JWTs + +To pass in an encoded JWT, use the token field of the context. +When using Apollo Server, extract the authorization header into the token property of the context: + +[source, javascript, indent=0] +---- +const server = new ApolloServer({ + schema, +}); + +await startStandaloneServer(server, { + context: async ({ req }) => ({ token: req.headers.authorization }), +}); +---- + +For example, a HTTP request with the following `authorization` header should look like this: + +[source] +---- +POST / HTTP/1.1 +authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI +content-type: application/json +---- + +Alternatively, you can pass a key `jwt` of type `JwtPayload` into the context, which has the following definition: + +[source, typescript, indent=0] +---- +// standard claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 +interface JwtPayload { + [key: string]: any; + iss?: string | undefined; + sub?: string | undefined; + aud?: string | string[] | undefined; + exp?: number | undefined; + nbf?: number | undefined; + iat?: number | undefined; + jti?: string | undefined; +} +---- + +[WARNING] +==== +Do not pass in the header or the signature. +==== + +=== Decoded JWTs + +A decoded JWT is passed to the context in a similar way that an encoded JWT is. +However, instead of using a token, it uses the `jwt` field: + +[source, typescript, indent=0] +---- +const jwt = customImplementation(); + +const { url } = await startStandaloneServer(server, { + listen: { port: 4000 }, + context: async ({ req }) => ({ + jwt: jwt, + }), +}); +---- + +`customImplementation` is a placeholder for a function that provides a decoded JWT. +Using `jwt` instead of `token` in the `context` informs the Neo4j GraphQL Library that it doesn't need to decode it. + +== Adding JWT claims By default, filtering is available on https://www.rfc-editor.org/rfc/rfc7519#section-4.1[the registered claim names] in the JWT specification. Filtering can be configured for additional JWT claims using the `@jwt` directive and, in some circumstances, the `@jwtClaim` directive. -If you configure an additional `roles` claim, which is an array of strings located at the root of the JWT payload, the following must be added to the type definitions: +=== The `@jwt` directive + +If you configure an additional `roles` claim, which is an array of strings located at the root of the JWT payload, add the following to the type definitions: [source, graphql, indent=0] ---- @@ -78,11 +157,16 @@ type JWT @jwt { } ---- -Note that the type name here, `JWT`, is not required, and this can have any name as long as it is decorated with the `@jwt` directive. +[NOTE] +==== +The type name `JWT` is not mandatory. +You can use any name as long as it is decorated with the `@jwt` directive. +==== -=== Nested claims +=== The `@jwtClaim` directive -If the previous `roles` claim is not located at the JWT payload root, but instead in a nested location, for example: +A `roles` claim is not necessarily located at the JWT payload root. +It can instead be in a nested location, for example under `myApplication`: [source, json, indent=0] ---- @@ -94,7 +178,7 @@ If the previous `roles` claim is not located at the JWT payload root, but instea } ---- -This needs to be configured using the `@jwtClaim` directive: +In this case, use the `@jwtClaim` directive alongside the `@jwt` directive: [source, graphql, indent=0] ---- @@ -103,7 +187,7 @@ type JWT @jwt { } ---- -Additionally, if this nested location contains any `.` characters in the path, for example: +Additionally, the nested location may contain `.` characters in the path, for example: [source, json, indent=0] ---- @@ -115,7 +199,7 @@ Additionally, if this nested location contains any `.` characters in the path, f } ---- -These characters need to be escaped: +These characters must be escaped: [source, graphql, indent=0] ---- @@ -126,50 +210,6 @@ type JWT @jwt { [NOTE] ==== -The seemingly excessive escaping is required to doubly escape: once for GraphQL and once for `dot-prop`, which is used under the hood to resolve the path. +The `path` must be escaped twice: once for GraphQL and once for `dot-prop`, which is used under the hood to resolve the path. ==== -== Passing in JWTs - -To pass in an encoded JWT, you must use the token field of the context. -When using Apollo Server, extract the authorization header into the token property of the context as follows: - -[source, javascript, indent=0] ----- -const server = new ApolloServer({ - schema, -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ token: req.headers.authorization }), -}); ----- - -For example, a HTTP request with the following `authorization` header should look like this: - -[source] ----- -POST / HTTP/1.1 -authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI -content-type: application/json ----- - -Alternatively, you can pass a key `jwt` of type `JwtPayload` into the context, which has the following definition: - -[source, typescript, indent=0] ----- -// standard claims https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 -interface JwtPayload { - [key: string]: any; - iss?: string | undefined; - sub?: string | undefined; - aud?: string | string[] | undefined; - exp?: number | undefined; - nbf?: number | undefined; - iat?: number | undefined; - jti?: string | undefined; -} ----- - -[WARNING] -_Do not_ pass in the header or the signature. diff --git a/modules/ROOT/pages/security/impersonation-and-user-switching.adoc b/modules/ROOT/pages/security/impersonation-and-user-switching.adoc index 34ec4863..7d0585e5 100644 --- a/modules/ROOT/pages/security/impersonation-and-user-switching.adoc +++ b/modules/ROOT/pages/security/impersonation-and-user-switching.adoc @@ -10,7 +10,7 @@ Impersonation still authenticates with the database as the original configured u When impersonating a user, the query is run within the complete security context of the impersonated user and not the authenticated user (home database, permissions etc). Consider the following an example of how to impersonate a different user per request. -Here the user to impersonate is taken from a HTTP header `User`: +Here the user to impersonate is taken from an HTTP header `User`: [.tabbed-example] ==== diff --git a/modules/ROOT/pages/security/index.adoc b/modules/ROOT/pages/security/index.adoc index 49603558..9288e803 100644 --- a/modules/ROOT/pages/security/index.adoc +++ b/modules/ROOT/pages/security/index.adoc @@ -5,10 +5,9 @@ auth/authorization.adoc, auth/auth-directive.adoc, auth/subscriptions.adoc, \ auth/authorization/allow.adoc, auth/authorization/bind.adoc, auth/authorization/roles.adoc, \ auth/authorization/where.adoc, authentication-and-authorization/index.adoc - -* xref::/security/authentication.adoc[Authentication] - Explicit authentication, configured using the `@authentication` directive. -* xref::/security/authorization.adoc[Authorization] - Authorization rules set using the `@authorization` directive. -* xref::/security/subscriptions-authorization.adoc[Subscriptions authorization] - Authorization rules for subscriptions set using the `@subscriptionsAuthorization` directive. -* xref::/security/configuration.adoc[Configuration] - Instructions to set up instantiation. +* xref::/security/configuration.adoc[Configuration] - Configuration of JSON Web Token (JWT) authentication with encoded or decoded JWTs. +* xref::/security/authentication.adoc[Authentication] - Explicit authentication for different operations on different parts of your schema with the `@authentication` directive. +* xref::/security/authorization.adoc[Authorization] - Rule-based authorization filtering and validation with the `@authorization` directive. +* xref::/security/subscriptions-authorization.adoc[Subscriptions authorization] - Rule-based authorization for subscriptions with the `@subscriptionsAuthorization` directive. * xref::/security/impersonation-and-user-switching.adoc[Impersonation and user switching] - How to set up impersonation and user switching features. -* xref::/security/operations.adoc[Operations] - Reference on GraphQL queries and how each location in each query triggers the evaluation of different authentication/authorization rules. +* xref::/security/operations.adoc[Operations] - GraphQL query examples on how to trigger the evaluation of different authentication and authorization rules. \ No newline at end of file diff --git a/modules/ROOT/pages/security/operations.adoc b/modules/ROOT/pages/security/operations.adoc index 70ffa941..5c40095b 100644 --- a/modules/ROOT/pages/security/operations.adoc +++ b/modules/ROOT/pages/security/operations.adoc @@ -2,9 +2,9 @@ //:page-aliases: /authentication-and-authorization/reference/operations.adoc, /security/reference/operations.adoc :description: This page describes how to set up authorization operations in the Neo4j GraphQL Library. -This reference runs through a number of worked GraphQL queries and how each location in each query triggers the evaluation of different authentication/authorization rules. +This page showcases a number of GraphQL queries and how you can trigger the evaluation of different authentication and authorization rules. -Each relevant location has a comment such as `CREATE ON OBJECT Movie`, which means an authentication directive such as the following would be evaluated: +Each relevant line has a comment such as `CREATE ON OBJECT Movie`, which means an authentication directive like the following is evaluated: [source, graphql, indent=0] ---- @@ -14,7 +14,10 @@ type Movie @authentication(operations: [CREATE]) { } ---- -This also applies if the directive had no arguments, because `operations` defaults to _all_ operations. +[IMPORTANT] +==== +This also applies if the directive has no arguments because `operations` defaults to _all_ operations. +==== The following examples apply to the `@authentication` directive, and also any rules within an `@authorization` directive. @@ -22,7 +25,7 @@ The following examples apply to the `@authentication` directive, and also any ru === Query -For a simple query, rules with `READ` in the operations will be evaluated for any type being read: +For a simple query, rules with `READ` in the operations are evaluated for any type being read: [source, graphql, indent=0] ---- @@ -38,7 +41,7 @@ query { === Mutation -For create Mutations, `CREATE` rules on the object will be evaluated for each node created, as well as field definition rules: +For `create` mutations, `CREATE` rules on the object are evaluated for each node created, as well as field definition rules: [source, graphql, indent=0] ---- @@ -55,7 +58,7 @@ mutation { } ---- -For single delete Mutations, rules with `DELETE` on the object will be evaluated: +For single `delete` mutations, rules with `DELETE` on the object are evaluated: [source, graphql, indent=0] ---- @@ -66,7 +69,7 @@ mutation { } ---- -For delete Mutations with nested delete operations, rules with operation `DELETE` will be evaluated: +For `delete` mutations with nested delete operations, rules with operation `DELETE` are evaluated: [source, graphql, indent=0] ---- @@ -80,7 +83,7 @@ mutation { } ---- -For a complex update Mutation with many effects, a variety of rules will be evaluated, as well as `READ` rules for the selection set: +For a complex `update` mutation with many effects, a variety of rules is evaluated, as well as `READ` rules for the selection set: [source, graphql, indent=0] ---- @@ -104,7 +107,7 @@ mutation { === Subscription -For a simple Subscription to creation events, both `SUBSCRIBE` and `READ` operations trigger rules: +For a simple subscription to creation events, both `SUBSCRIBE` and `READ` operations trigger rules: [source, graphql, indent=0] ---- @@ -117,7 +120,7 @@ subscription { } ---- -For a more complex Subscription to relationship events, both `SUBSCRIBE` is an operation, as well as `READ` to all relevant types: +For a more complex subscription to relationship events, `SUBSCRIBE` is an operation as well as `READ` to all relevant types: [source, graphql, indent=0] ----