diff --git a/.changeset/pre.json b/.changeset/pre.json index 8c71b6448b..54f9aaae5e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,22 +1,21 @@ { - "mode": "pre", - "tag": "beta", - "initialVersions": { - "graphql-manual": "3.0.0", - "migration": "0.0.1", - "neo-place": "0.1.0", - "neo-push-client": "1.0.0", - "neo-push-server": "1.0.0", - "apollo-suscriptions-server": "1.0.0", - "yoga-subscriptions-server": "1.0.0", - "@neo4j/cypher-builder": "0.1.8", - "@neo4j/graphql": "3.14.1", - "@neo4j/graphql-toolbox": "1.4.6", - "@neo4j/introspector": "1.0.2", - "@neo4j/graphql-ogm": "3.14.1", - "@neo4j/package-tests": "1.0.0", - "@neo4j/graphql-plugin-auth": "1.1.1", - "@neo4j/graphql-plugin-subscriptions-amqp": "1.0.0" - }, - "changesets": [] + "mode": "pre", + "tag": "beta", + "initialVersions": { + "graphql-manual": "3.0.0", + "migration": "0.0.1", + "neo-place": "0.1.0", + "neo-push-client": "1.0.0", + "neo-push-server": "1.0.0", + "apollo-suscriptions-server": "1.0.0", + "yoga-subscriptions-server": "1.0.0", + "@neo4j/cypher-builder": "0.1.8", + "@neo4j/graphql": "3.14.1", + "@neo4j/graphql-toolbox": "1.4.6", + "@neo4j/introspector": "1.0.2", + "@neo4j/graphql-ogm": "3.14.1", + "@neo4j/package-tests": "1.0.0", + "@neo4j/graphql-plugin-subscriptions-amqp": "1.0.0" + }, + "changesets": [] } diff --git a/.github/labeler.yml b/.github/labeler.yml index 7506476436..0d19ed9ea4 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -23,6 +23,3 @@ labels: - label: introspector files: - packages/introspector/src/.* - - label: plugin-auth - files: - - packages/graphql-plugin-auth/src/.* diff --git a/.github/workflows/reusable-unit-tests.yml b/.github/workflows/reusable-unit-tests.yml index e1c03e4497..3a9a805d34 100644 --- a/.github/workflows/reusable-unit-tests.yml +++ b/.github/workflows/reusable-unit-tests.yml @@ -24,7 +24,6 @@ jobs: - graphql - ogm - introspector - - plugins/graphql-plugin-auth - plugins/graphql-plugin-subscriptions-amqp runs-on: ubuntu-latest diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index ab3d4d6ba7..35dbbfd6ea 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -16,7 +16,6 @@ jobs: - graphql - introspector - ogm - - plugins/graphql-plugin-auth - plugins/graphql-plugin-subscriptions-amqp runs-on: ubuntu-latest diff --git a/.yarn/versions/c014b267.yml b/.yarn/versions/c014b267.yml index 2140e94d83..a55095006a 100644 --- a/.yarn/versions/c014b267.yml +++ b/.yarn/versions/c014b267.yml @@ -3,6 +3,5 @@ undecided: - neo-push-client - neo-push-server - "@neo4j/graphql" - - "@neo4j/graphql-plugin-auth" - "@neo4j/introspector" - "@neo4j/package-tests" diff --git a/README.md b/README.md index 0d4f875116..8d5d401c36 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,6 @@ visit the [Contributing Guide](./CONTRIBUTING.md). 2. [`@neo4j/graphql-ogm`](./packages/ogm) - Use GraphQL Type Definitions to drive interactions with the database 3. [`@neo4j/introspector`](./packages/introspector) - Introspect schema from an existing Neo4j database -4. [`@neo4j/graphql-plugin-auth`](./packages/graphql-plugin-auth) - Auth decode plugins for @neo4j/graphql 5. [`@neo4j/graphql-toolbox`](./packages/graphql-toolbox) - Experiment with your Neo4j GraphQL API on Neo4j Desktop. ## Media diff --git a/docs/modules/ROOT/content-nav.adoc b/docs/modules/ROOT/content-nav.adoc index d216794a13..c928d5f0b1 100644 --- a/docs/modules/ROOT/content-nav.adoc +++ b/docs/modules/ROOT/content-nav.adoc @@ -27,17 +27,6 @@ *** xref:pagination/offset-based.adoc[] *** xref:pagination/cursor-based.adoc[] -** xref:auth/index.adoc[] -*** xref:auth/getting-started.adoc[] -**** xref:auth/auth-directive-subs.adoc[] -*** xref:auth/authorization/index.adoc[] -**** xref:auth/authorization/allow.adoc[] -**** xref:auth/authorization/bind.adoc[] -**** xref:auth/authorization/roles.adoc[] -**** xref:auth/authorization/where.adoc[] -*** xref:auth/authentication.adoc[] -*** xref:auth/subscriptions.adoc[] - ** Authentication and Authorization *** xref:authentication-and-authorization/configuration.adoc[] *** xref:authentication-and-authorization/authentication.adoc[] diff --git a/docs/modules/ROOT/pages/auth/auth-directive-subs.adoc b/docs/modules/ROOT/pages/auth/auth-directive-subs.adoc deleted file mode 100644 index 3f0be38be0..0000000000 --- a/docs/modules/ROOT/pages/auth/auth-directive-subs.adoc +++ /dev/null @@ -1,96 +0,0 @@ -[[auth-directive-subsctiptions]] -= `@auth` directive and Subscriptions - -== `@auth` directive - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -The `@auth` directive definition is dynamically generated on runtime based on user type definitions. - -=== `rules` - -You can have many rules for many operations. Each rule is fallen through until a match is found against the corresponding operation. If no match is found, an error is thrown. You can think of rules as a big `OR`. - -[source, graphql, indent=0] ----- -@auth(rules: [ - { operations: [CREATE, UPDATE], ... }, ## or - { operations: [READ, UPDATE], ...}, ## or - { operations: [DELETE, UPDATE], ... } ## or -]) ----- - -=== `operations` - -`operations` is an array which allows you to re-use the same rule for many operations. - -[source, graphql, indent=0] ----- -@auth(rules: [ - { operations: [CREATE, UPDATE, DELETE, CONNECT, DISCONNECT, SUBSCRIBE] }, - { operations: [READ] } -]) ----- - -NOTE: Note that the absence of an `operations` argument will imply _all_ operations. - -Many different operations can be called at once, for example in the following mutation: - -[source, graphql, indent=0] ----- -mutation { - createPosts( - input: [ - { - content: "I like GraphQL", - creator: { connect: { where: { id: "user-01" } } } - } - ] - ) { - posts { - content - } - } -} ----- - -In the above example, there is a `CREATE` operation followed by a `CONNECT`, so the auth rule must allow a user to perform both of these operations. - -=== Auth Value Plucking - -When using the `@auth` directive, you use the following prefixes to substitute in their relevant values: - -- `$jwt.` - pulls value from JWT -- `$context.` - pulls value from context - - -[[subscriptions]] -== Subscriptions - -xref::subscriptions/index.adoc[Subscriptions] can be used along with `@auth`, however, some operations are not supported. To setup rules, -use the `SUBSCRIBE` operation. - -```graphql -type Movie { - title: String! -} - -extend type Movie @auth(rules: [{ isAuthenticated: true, operations: [SUBSCRIBE] }]) -``` - -=== Authentication -If the authentication rules `isAuthenticated` and `allowUnauthenticated` are not met, the subscription request will fail and no events will -be sent. - -=== Roles -Roles can be set for subscriptions. Only requests matching the roles set will be accepted. - -=== Bind -NOTE: Not Supported - -=== Where -NOTE: Not Supported - -=== Allow -NOTE: Not Supported diff --git a/docs/modules/ROOT/pages/auth/authentication.adoc b/docs/modules/ROOT/pages/auth/authentication.adoc deleted file mode 100644 index 4a4a7b1b9c..0000000000 --- a/docs/modules/ROOT/pages/auth/authentication.adoc +++ /dev/null @@ -1,97 +0,0 @@ -[[auth-authentication]] -= Authentication - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -The Neo4j GraphQL Library expects an `authorization` header in the request object, which means you can authenticate users however you like. You could have a custom sign-in mutation, integrate with Auth0, or roll your own SSO server. The point here is that it’s just a JWT which the library decodes to make sure it’s valid - but it’s down to the user to issue tokens. - -> The example at xref::ogm/examples.adoc#custom-resolvers[Custom Resolvers] demonstrates a hypothetical sign-up/sign-in flow using the xref::ogm/index.adoc[OGM], which will be a good starting point for inspiration. - -== `isAuthenticated` - -This is the most basic of authentication, used to ensure that there is a valid decoded JWT in the request. The most basic of type definitions could look something like the following, which states you must be authenticated to access `Todo` objects: - -[source, graphql, indent=0] ----- -type Todo { - id: ID - title: String -} - -extend type Todo @auth(rules: [{ isAuthenticated: true }]) ----- - -== `allowUnauthenticated` - -In some cases, you may want to allow unauthenticated requests while also having auth-based rules. You can use the `allowUnauthenticated` parameter to avoid throwing an exception if no auth is present in the context. - -In the example below, only the publisher can see his blog posts if it is not published yet. Once the blog post is published, anyone can see it: - -[source, graphql, indent=0] ----- -type BlogPost - @auth( - rules: [ - { - operations: [READ] - where: { OR: [{ publisher: "$jwt.sub" }, { published: true }] } - allowUnauthenticated: true - } - ] - ) { - id: ID! - publisher: String! - published: Boolean! -} ----- - -[[auth-global-authentication]] -== Global Authentication - -For some cases the GraphQL API needs to be secured globally to restrict access to _any_ of the top-level GraphQL types without prior authentication. In the Neo4j GraphQL Library this is referred to as global authentication. It is also known as API-wide authorization. - -=== Configuration - -To use the global authentication functionality, it is required to have an instance of an auth plugin for the Neo4j GraphQL Library. For most use cases you will only need to use our provided plugins at `@neo4j/graphql-plugin-auth`. Below is an example configuration enabling global authentication via the `Neo4jGraphQLAuthJWTPlugin` class: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "super-secret", - globalAuthentication: true, - }) - } -}); ----- - -NOTE: Observe that the `Neo4jGraphQLAuthJWTPlugin` class does not accept to enable both `noVerify` and `globalAuthentication` simultaneously. - -If you would like to use JWKS decoding and enable global authentication then use the `Neo4jGraphQLAuthJWKSPlugin` class like so: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - globalAuthentication: true, - }) - } -}); ----- - -=== Functionality - -If global authentication is enabled in the auth plugin for the Neo4j GraphQL Library, it is required that _each_ request contains a valid JWT token in the `authorization` header. Otherwise an authentication error will be thrown. - diff --git a/docs/modules/ROOT/pages/auth/authorization/allow.adoc b/docs/modules/ROOT/pages/auth/authorization/allow.adoc deleted file mode 100644 index 7dce0fc2a8..0000000000 --- a/docs/modules/ROOT/pages/auth/authorization/allow.adoc +++ /dev/null @@ -1,144 +0,0 @@ -[[auth-authorization-allow]] -= Allow - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -Use `allow` to ensure that on matched nodes, there is equality between a value on the JWT and a property on each matched node. Taking a closer look, create two users in a hypothetical empty database: - -[source, cypher, indent=0] ----- -CREATE (:User { id: "user1", name: "one" }) -CREATE (:User { id: "user2", name: "two" }) ----- - -For the label and properties of the nodes created above, the corresponding GraphQL type definition would be: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! -} ----- - -Now that there are two users in the database, and a simple type definition - it might be desirable to restrict `user1` from accessing `user2`. This is where `allow` comes in: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! -} - -extend type User @auth( - rules: [ - { - operations: [READ], - allow: { id: "$jwt.sub" } - } - ] -) ----- - -After a match is made against a node, it is validated that the property `id` on the node is equal to the `jwt.sub` property. - -Given `user1` has the following decoded JWT: - -[source, json, indent=0] ----- -{ - "sub": "user1", - "iat": 1516239022 -} ----- - -If "user1" used this JWT in a request for "user2": - -[source, graphql, indent=0] ----- -query { - users(where: { id: "user2" }) { - name - } -} ----- - -The generated cypher for this query would look like the following and throw you out the operation: - -[source, cypher, indent=0] ----- -MATCH (u:User { id: "user2" }) -CALL apoc.util.validate(NOT (u.id = "user1"), "Forbidden") -RETURN u ----- - -Allow is available on the following operations: - -- `READ` -- `UPDATE` -- `CONNECT` -- `DISCONNECT` -- `DELETE` - -== `allow` across relationships - -There may be a reason where you need to traverse across relationships to satisfy your authorization implementation. One example use case could be "grant update access to all Moderators of a Post": - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -type Post { - content: String - moderators: [User!]! @relationship(type: "MODERATES_POST", direction: IN) -} - -extend type Post @auth(rules: [ - { operations: [UPDATE], allow: { moderators: { id: "$jwt.sub" } } } -]) ----- - -When you specify allow on a relationship you can select fields on the referenced node. It's worth pointing out that allow on a relationship will perform an `ANY` on the matched nodes to see if there is a match. - -Given the above example - There may be a time when you need to give update access to either the creator of a post or a moderator, you can use `OR` and `AND` inside `allow`: - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -type Post { - content: String - moderators: [User!]! @relationship(type: "MODERATES_POST", direction: IN) - creator: User! @relationship(type: "HAS_POST", direction: IN) -} - -extend type Post - @auth( - rules: [ - { - operations: [UPDATE], - allow: { OR: [{ moderators: { id: "$jwt.sub" } }, { creator: { id: "$jwt.sub" } }] } - } - ] - ) ----- - -== Field-level `allow` - -`allow` works the same as it does on Types although its context is the Field. So instead of enforcing auth rules when the node is matched and/or modified, it would instead be called when the Field is match and/or modified. Given the following, it is hiding the password to all users but the user themselves: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! - password: String! @auth(rules: [{ allow: { id: "$jwt.sub" } }]) -} ----- diff --git a/docs/modules/ROOT/pages/auth/authorization/bind.adoc b/docs/modules/ROOT/pages/auth/authorization/bind.adoc deleted file mode 100644 index 32b036d2ee..0000000000 --- a/docs/modules/ROOT/pages/auth/authorization/bind.adoc +++ /dev/null @@ -1,119 +0,0 @@ -[[auth-authorization-bind]] -= Bind - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -Use bind to ensure that on creating or updating nodes, there is equality between a value on the JWT and a property on a matched node. This validation is done after the operation but inside a transaction. Taking a closer look, create a user in your database: - -[source, cypher, indent=0] ----- -CREATE (:User { id:"user1", name: "one" }) ----- - -For the label and properties of the node created above, the corresponding GraphQL type definitions would be: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! -} ----- - -Given the above GraphQL type definition - you could restrict `user1` from changing their own ID: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! -} - -extend type User @auth( - rules: [ - { - operations: [UPDATE], - bind: { id: "$jwt.sub" } - } - ] -) ----- - -After the update or creation of the node, it is validated that the property `id` on the node is equal to the `jwt.sub` property. - -Given `user1` has the following decoded JWT: - -[source, json, indent=0] ----- -{ - "sub": "user1", - "iat": 1516239022 -} ----- - -When the user makes a request using this JWT to change their ID: - -[source, graphql, indent=0] ----- -mutation { - updateUsers(where: { id: "user1" }, update: { id: "user2" }) { - users { - name - } - } -} ----- - -The generated cypher for this query would look like the below, throwing you out of the operation because the `id` property no longer matches. - -[source, cypher, indent=0] ----- -MATCH (u:User { id: "user1" }) -SET u.id = "user2" -CALL apoc.util.validate(NOT (u.id = "user1"), "Forbidden") -RETURN u ----- - -Bind is available for the following operations; - -- `READ` -- `UPDATE` -- `CONNECT` -- `DISCONNECT` -- `DELETE` - -== `bind` across relationships - -There may be a reason where you need to traverse across relationships to satisfy your authorization implementation. One use case could be "ensure that users only create Posts related to themselves": - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -type Post { - content: String - creator: User! @relationship(type: "HAS_POST", direction: IN) -} - -extend type Post @auth(rules: [ - { operations: [CREATE], bind: { creator: { id: "$jwt.sub" } } } -]) ----- - -When you specify `bind` on a relationship you can select fields on the related node. It's worth pointing out that `bind` on a relationship field will perform an `all` on the matched nodes to see if there is a match, or `any` if the `bindPredicate` option of the plugin has been set to "any". - -=== Field-level `bind` - -You can use `bind` on a field, and the root is still considered the node itself. Taking the example at the start of this chapter, you could do the following to implement the same behaviour: - -[source, graphql, indent=0] ----- -type User { - id: ID! @auth(rules: [{ operations: [UPDATE], bind: { id: "$jwt.sub" } }]) - name: String! -} ----- diff --git a/docs/modules/ROOT/pages/auth/authorization/index.adoc b/docs/modules/ROOT/pages/auth/authorization/index.adoc deleted file mode 100644 index 1bdefd990c..0000000000 --- a/docs/modules/ROOT/pages/auth/authorization/index.adoc +++ /dev/null @@ -1,14 +0,0 @@ -[[auth-authorization]] -= Authorization - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -You specify authorization rules inside the `@auth` directive. This section looks at each option available and explains how to use it to implement authorization. - -- xref::auth/authorization/allow.adoc[Allow] -- xref::auth/authorization/bind.adoc[Bind] -- xref::auth/authorization/roles.adoc[Roles] -- xref::auth/authorization/where.adoc[Where] - -NOTE: Note that authorization rules are not supported when applied over nested xref::reference/type-definitions/unions.adoc[unions] or xref::reference/type-definitions/interfaces.adoc[interfaces]. diff --git a/docs/modules/ROOT/pages/auth/authorization/roles.adoc b/docs/modules/ROOT/pages/auth/authorization/roles.adoc deleted file mode 100644 index 604a7af742..0000000000 --- a/docs/modules/ROOT/pages/auth/authorization/roles.adoc +++ /dev/null @@ -1,50 +0,0 @@ -[[auth-authorization-roles]] -= Roles - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -Use the `roles` property to specify the allowed roles for an operation. Use the `Neo4jGraphQL` config option `rolesPath` to specify a object path for JWT roles otherwise defaults to `jwt.roles`. - -The following type definitions show that an admin role is required for all update operations against Users. - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -extend type User @auth(rules: [{ operations: [UPDATE], roles: ["admin"] }]) ----- - -If there are multiple possible roles you can add more items to the array, of which users only need one to satisfy a rule: - -[source, graphql, indent=0] ----- -extend type User @auth(rules: [{ operations: [UPDATE], roles: ["admin", "super-admin"] }]) ----- - -== RBAC - -Here is an example of RBAC (Role-Based Access Control) using `roles`: - -[source, graphql, indent=0] ----- -type CatalogItem @auth(rules: [{ operations: [READ], roles: ["read:catalog"] }]) { - id: ID - title: String -} - -type Customer @auth(rules: [{ operations: [READ], roles: ["read:customer"] }]) { - id: ID - name: String - password: String @auth(rules: [{ operations: [READ], roles: ["admin"] }]) -} - -type Invoice @auth(rules: [{ operations: [READ], roles: ["read:invoice"] }]) { - id: ID - csv: String - total: Int -} ----- diff --git a/docs/modules/ROOT/pages/auth/authorization/where.adoc b/docs/modules/ROOT/pages/auth/authorization/where.adoc deleted file mode 100644 index 88f430dc3f..0000000000 --- a/docs/modules/ROOT/pages/auth/authorization/where.adoc +++ /dev/null @@ -1,102 +0,0 @@ -[[auth-authorization-where]] -= Where - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -Use the `where` argument on types to conceptually append predicates to the Cypher `WHERE` clause. Given the current user ID is "123" and the following schema: - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -extend type User @auth(rules: [{ where: { id: "$jwt.id" } }]) ----- - -Then the user executes a GraphQL query for all users: - -[source, graphql, indent=0] ----- -query { - users { - id - name - } -} ----- - -Behind the scenes the user’s ID is conceptually added to the query: - -[source, graphql, indent=0] ----- -query { - users(where: { id: "123" }){ - id - name - } -} ----- - -Where is used on the following operations; - -- `READ` -- `UPDATE` -- `CONNECT` -- `DISCONNECT` -- `DELETE` - - -== Combining `where` with `roles` - -The `where` argument can be combined with `roles` within auth rules to support rule based filtering of results. - -Revising the early example schema, if you update the auth rules to instead provide two sets of rules for two different roles, you can specify that JWTs with the `user` role can only see their own `User` node, as before, but now, those with the `admin` role can see all users: - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -extend type User @auth(rules: [ - { - roles: ["user"] - where: { id: "$jwt.id" } - } - { - roles: ["admin"] - } -]) ----- - -For those with an admin role, there is a conceptual `where: "*"` rule which is applied, allowing admins to see all `User` nodes. - -These rules can alternatively be expressed using an `OR` within the top parent of a single auth rule, for example: - -[source, graphql, indent=0] ----- -type User { - id: ID - name: String -} - -extend type User @auth(rules: [ - { - OR: [ - { - roles: ["user"] - where: { id: "$jwt.id" } - }, - { - roles: ["admin"] - } - ] - } -]) ----- - -Both ways of expressing the rules are valid, and will return the same results. diff --git a/docs/modules/ROOT/pages/auth/getting-started.adoc b/docs/modules/ROOT/pages/auth/getting-started.adoc deleted file mode 100644 index 87f380f506..0000000000 --- a/docs/modules/ROOT/pages/auth/getting-started.adoc +++ /dev/null @@ -1,295 +0,0 @@ -[[auth-getting-started]] -= Getting Started - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::guides/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -== Configuration - -To get started with auth you need an instance of an auth plugin for the Neo4j GraphQL Library. For most use cases you will only need to use our provided plugins at `@neo4j/graphql-plugin-auth`. Below is a basic example using the `Neo4jGraphQLAuthJWTPlugin` class: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "super-secret" - }) - } -}); ----- - -Or you can initiate the secret with a function which will run to retrieve the secret when the request comes in. - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: (req) => { - return "super-secret"; - }, - }) - } -}); ----- - -If you would like to use JWKS decoding then use the `Neo4jGraphQLAuthJWKSPlugin` class: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - }) - } -}); ----- - -Or you can pass a function as `jskwsEndpoint` to compute the endpoint when the request comes in. - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: (req) => { - let url = "https://YOUR_DOMAIN/well-known/{file}.json"; - const fileHeader = req.headers["file"]; - url = url.replace("{file}", fileHeader); - return url; - }, - }), - } -}); ----- - -If you need to create your own auth plugin then ensure it adheres to the following interface: - -[source, javascript, indent=0] ----- -interface Neo4jGraphQLAuthPlugin { - rolesPath?: string; - isGlobalAuthenticationEnabled?: boolean; - - decode(token: string | any): Promise; -} ----- - -It is also possible to pass in JWTs which have already been decoded, in which case the `jwt` option is _not necessary_. This is covered in the section xref::auth/getting-started.adoc#auth-setup-passing-in[Passing in JWTs] below. Note that the plugin's base decode method only supports HS256 and RS256 algorithms. - -=== Optional Constructor Arguments - -Additionally, for both the `Neo4jGraphQLAuthJWTPlugin` and the `Neo4jGraphQLAuthJWKSPlugin` one may optionally specify an `issuer` and an `audience` constructor argument. These reference to the `iss` and the `aud` claim respectively in a JWT token, see https://www.rfc-editor.org/rfc/rfc7519#page-9[JWT RFC]. -If the `issuer` and/or the `audience` arguments are provided, they will be checked as part of the token verification. - -An example on how to provide an `issuer` for the `Neo4jGraphQLAuthJWKSPlugin` is shown below. In this case, any JWT token must contain the `iss` claim `"https://YOUR_DOMAIN"` for the token validation to be successful. -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - issuer: "https://YOUR_DOMAIN" - }), - }, -}); ----- - -=== Auth Roles Object Paths - -If you are using a 3rd party auth provider such as Auth0 you may find your roles property being nested inside an object: - -[source, json, indent=0] ----- -{ - "https://auth0.mysite.com/claims": { - "https://auth0.mysite.com/claims/roles": ["admin"] - } -} ----- - -In order to make use of this, you must pass it in as a "dot path" into the `rolesPath` option: - -[source, javascript, indent=0] ----- -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - rolesPath: "https://auth0\\.mysite\\.com/claims.https://auth0\\.mysite\\.com/claims/roles" - }) - } -}); ----- - -Note that `.` characters within a key of the JWT must be escaped with `\\`, whilst a `.` character indicating traversal into a value must not be escaped. - -=== Cypher predicate used to evaluate `bind` rules - -By default, `bind` rules are evaluated using an `all` predicate in Cypher, which can lead to rules not being satisfied when they perhaps should, for instance only one related user matching the current JWT, rather than all of them. - -To avoid a breaking change to a security-critical feature like authorization, a flag, `bindPredicate`, has been exposed to switch this predicate to `any`, which can be used as follows: - -[source, javascript, indent=0] ----- -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "super-secret", - bindPredicate: "any" - }) - } -}); ----- - -In the next major release, this will become the default behaviour when evaluating `bind` rules. - -[[auth-setup-passing-in]] -== Passing in JWTs - -If you wish to pass in an encoded JWT, this must be included in the `authorization` header of your requests, in the format: - -[source] ----- -POST / HTTP/1.1 -authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI -content-type: application/json ----- - -Note the string "Bearer" before the inclusion of the JWT. - -Then, using Apollo Server as an example, you must include the request in the GraphQL context, as follows (using the `neoSchema` instance from the example above): - -[source, javascript, indent=0] ----- -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ req }), -}); ----- - -Note that the request key `req` is appropriate for Express servers, but different middlewares use different keys for request objects. You can more details at https://www.apollographql.com/docs/apollo-server/api/apollo-server/#middleware-specific-context-fields. - -=== Decoded JWTs - -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; -} ----- - -_Do not_ pass in the header or the signature. - -For example, you might have a function `decodeJWT` which returns a decoded JWT: - -[source, javascript, indent=0] ----- -const decodedJWT = decodeJWT(encodedJWT) - -const server = new ApolloServer({ - schema: await neoSchema.getSchema(), -}); - -await startStandaloneServer(server, { - context: async ({ req }) => ({ req, jwt: decodedJWT.payload }), -}); ----- - -== Auth and Custom Resolvers - -You can't use the `@auth` directive on custom resolvers, however, an auth parameter is injected into the context for use in them. It will be available under the `auth` property. For example, the following custom resolver returns the `sub` field from the JWT: - -[source, javascript, indent=0] ----- -const typeDefs = `#graphql - type Query { - myId: ID! - } -`; - -const resolvers = { - Query: { - myId(_source, _args, context) { - return context.auth.jwt.sub - } - } -}; ----- - -== Auth and `@cypher` fields - -You can put the `@auth` directive on a field alongside the `@cypher` directive. Functionality like `allow` and `bind` will not work but you can still utilize `isAuthenticated` and `roles`. Additionally, you don't need to specify `operations` for `@auth` directives on `@cypher` fields. - -The following example uses the `isAuthenticated` rule to ensure a user is authenticated, before returning the `User` associated with the JWT: - -[source, graphql, indent=0] ----- -type User @exclude { - id: ID - name: String -} - -type Query { - me: User - @cypher(statement: "MATCH (u:User { id: $auth.jwt.sub }) RETURN u") - @auth(rules: [{ isAuthenticated: true }]) -} ----- - -In the following example, the current user must have role "admin" in order to query the `history` field on the type `User`: - -[source, graphql, indent=0] ----- -type History @exclude { - website: String! -} - -type User { - id: ID - name: String - history: [History] - @cypher(statement: "MATCH (this)-[:HAS_HISTORY]->(h:History) RETURN h") - @auth(rules: [{ roles: ["admin"] }]) -} ----- diff --git a/docs/modules/ROOT/pages/auth/index.adoc b/docs/modules/ROOT/pages/auth/index.adoc deleted file mode 100644 index 9dc2a8241d..0000000000 --- a/docs/modules/ROOT/pages/auth/index.adoc +++ /dev/null @@ -1,51 +0,0 @@ -[[auth]] -= Auth (deprecated) - -WARNING: The `@auth` directive has been replaced by `@authentication` and `@authorization`. `@auth` will be removed in version 4.0.0. -Please see the xref::migration/v4-migration/authorization.adoc[upgrade guide] for details on how to upgrade. - -In this chapter you will learn more about how to secure your GraphQL API using the Neo4j GraphQL Library's built-in auth mechanics. - -== Quickstart examples - -Only authenticated users can create Post nodes: - -[source, graphql, indent=0] ----- -type Post @auth(rules: [ - { operations: [CREATE], isAuthenticated: true } -]) { - title: String! -} ----- - -Use `extend` to avoid large and unwieldy type definitions: - -[source, graphql, indent=0] ----- -type Post { - title: String! -} - -extend type Post @auth(rules: [ - { operations: [CREATE], isAuthenticated: true } -]) ----- - -You can use the directive types as seen in the example above, but you can also apply the directive on any field so as long as it's not decorated with `@relationship`. In the following example, the password field is only accessible to users with role "admin", or the user themselves: - -[source, graphql, indent=0] ----- -type User { - id: ID! - name: String! -} - -extend type User { - password: String! @auth(rules: [ - { - OR: [{ roles: ["admin"] }, { allow: { id: "$jwt.sub" } }] - } - ]) -} ----- diff --git a/examples/neo-place/package.json b/examples/neo-place/package.json index 0f07e367d6..3e881f6583 100644 --- a/examples/neo-place/package.json +++ b/examples/neo-place/package.json @@ -37,7 +37,6 @@ "dependencies": { "@apollo/server": "^4.7.0", "@neo4j/graphql": "^3.15.0", - "@neo4j/graphql-plugin-auth": "^2.1.0", "@neo4j/graphql-plugin-subscriptions-amqp": "^2.0.0", "dotenv": "^16.0.3", "express": "^4.18.2", diff --git a/examples/neo-place/server.js b/examples/neo-place/server.js index 80a1d73084..b539cce2dd 100644 --- a/examples/neo-place/server.js +++ b/examples/neo-place/server.js @@ -8,7 +8,6 @@ const { useServer } = require("graphql-ws/lib/use/ws"); const express = require("express"); const { ApolloServer } = require("apollo-server-express"); const { ApolloServerPluginDrainHttpServer } = require("apollo-server-core"); -const { Neo4jGraphQLAuthJWTPlugin } = require("@neo4j/graphql-plugin-auth"); const setupMap = require("./map-setup"); const { getDriver } = require("./get-driver"); @@ -25,11 +24,9 @@ async function main() { const neoSchema = new Neo4jGraphQL({ typeDefs: typeDefs, driver: driver, - plugins: { - subscriptions: plugin, - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "super-secret42", - }), + features: { + authorization: { key: "super-secret42" }, + subscriptions: plugin }, }); @@ -64,7 +61,7 @@ async function main() { const server = new ApolloServer({ schema, - context: ({ req }) => ({ req }), + context: ({ req }) => ({ token: req.headers.authorization }), plugins: [ ApolloServerPluginDrainHttpServer({ httpServer, diff --git a/examples/neo-place/typedefs.graphql b/examples/neo-place/typedefs.graphql index ee9a0bd7a4..2bd46c6f52 100644 --- a/examples/neo-place/typedefs.graphql +++ b/examples/neo-place/typedefs.graphql @@ -14,7 +14,7 @@ type Query { """ columnName: "canvas" ) - @auth(rules: [{ isAuthenticated: true }]) + @authentication } -extend type Pixel @auth(rules: [{ isAuthenticated: true }]) +extend type Pixel @authentication diff --git a/jest.config.base.js b/jest.config.base.js index eb929b20b6..ce209e5a86 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -18,7 +18,6 @@ module.exports = { "@neo4j/graphql/dist/types": "/packages/graphql/src/types", "@neo4j/introspector(.*)$": "/packages/introspector/src/$1", "@neo4j/graphql-ogm(.*)$": "/packages/ogm/src/$1", - "@neo4j/graphql-plugin-auth(.*)$": "/packages/plugins/graphql-plugin-auth/src/$1", "@neo4j/graphql-plugin-subscriptions-amqp(.*)$": "/packages/plugins/graphql-plugin-subscriptions-amqp/src/$1", "@neo4j/graphql(.*)$": "/packages/graphql/src/$1", diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 90f1d060e1..bf0229dddd 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -37,8 +37,7 @@ "test:e2e:watch": "jest e2e --watch", "test:schema": "jest tests/schema -c jest.minimal.config.js", "test:schema:watch": "jest tests/schema --watch -c jest.minimal.config.js", - "remove:graphql-plugin-auth": "node -p \"JSON.stringify({...require('./package.json')}, (k,v) => k === '@neo4j/graphql-plugin-auth' ? undefined : v, 2)\" > ./package.json.new && rm package.json && mv package.json.new package.json", - "setup:package-tests": "yarn pack && mv *.tgz ../package-tests/ && cd ../package-tests/ && rimraf package && tar -xvzf *.tgz && cd package && npm run remove:graphql-plugin-auth && cd ../ && yarn install && yarn run setup", + "setup:package-tests": "yarn pack && mv *.tgz ../package-tests/ && cd ../package-tests/ && rimraf package && tar -xvzf *.tgz && cd package && cd ../ && yarn install && yarn run setup", "posttest:package-tests": "yarn run cleanup:package-tests", "test:package-tests": "yarn run setup:package-tests && cd ../package-tests/ && yarn run test:all", "cleanup:package-tests": "cd ../package-tests/ && yarn run cleanup && rimraf ./package/ && rimraf *.tgz", @@ -53,7 +52,6 @@ "@apollo/gateway": "2.4.9", "@apollo/server": "4.7.5", "@faker-js/faker": "8.0.2", - "@neo4j/graphql-plugin-auth": "^2.2.0", "@types/deep-equal": "1.0.1", "@types/is-uuid": "1.0.0", "@types/jest": "29.5.3", @@ -71,6 +69,7 @@ "is-uuid": "1.0.2", "jest": "29.6.1", "jest-extended": "4.0.0", + "jwks-rsa": "^3.0.1", "koa": "2.14.2", "koa-jwt": "4.0.4", "koa-router": "12.0.0", diff --git a/packages/graphql/tests/integration/directives/alias/connect-or-create.int.test.ts b/packages/graphql/tests/integration/directives/alias/connect-or-create.int.test.ts index 16dfd3ce37..134b349239 100644 --- a/packages/graphql/tests/integration/directives/alias/connect-or-create.int.test.ts +++ b/packages/graphql/tests/integration/directives/alias/connect-or-create.int.test.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; import type { Driver, Session } from "neo4j-driver"; import { graphql } from "graphql"; import { UniqueType } from "../../../utils/graphql-types"; @@ -61,11 +60,6 @@ describe("@alias directive", () => { neoSchema = new Neo4jGraphQL({ typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts index 99ff64810e..8177ae93ff 100644 --- a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts +++ b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; import type { Driver, Session } from "neo4j-driver"; import { graphql } from "graphql"; import Neo4j from "../../neo4j"; @@ -66,11 +65,6 @@ describe("@alias directive", () => { neoSchema = new Neo4jGraphQL({ typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/directives/alias/unions.int.test.ts b/packages/graphql/tests/integration/directives/alias/unions.int.test.ts index 0bb2492de2..c1b9912125 100644 --- a/packages/graphql/tests/integration/directives/alias/unions.int.test.ts +++ b/packages/graphql/tests/integration/directives/alias/unions.int.test.ts @@ -18,7 +18,6 @@ */ import { gql } from "graphql-tag"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; import type { Driver, Session } from "neo4j-driver"; import { graphql } from "graphql"; import Neo4j from "../../neo4j"; @@ -75,11 +74,6 @@ describe("@alias directive", () => { `; neoSchema = new Neo4jGraphQL({ typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/directives/authorization/jwks-endpoint.int.test.ts b/packages/graphql/tests/integration/directives/authorization/jwks-endpoint.int.test.ts index 087a4f6c1c..5ae20af88d 100644 --- a/packages/graphql/tests/integration/directives/authorization/jwks-endpoint.int.test.ts +++ b/packages/graphql/tests/integration/directives/authorization/jwks-endpoint.int.test.ts @@ -26,7 +26,6 @@ import supertest from "supertest"; import Koa from "koa"; import Router from "koa-router"; import jwt from "koa-jwt"; - import jwksRsa from "jwks-rsa"; import Neo4j from "../../neo4j"; import { Neo4jGraphQL } from "../../../../src/classes"; diff --git a/packages/graphql/tests/integration/issues/1320.int.test.ts b/packages/graphql/tests/integration/issues/1320.int.test.ts index eea3680375..c01f10efdb 100644 --- a/packages/graphql/tests/integration/issues/1320.int.test.ts +++ b/packages/graphql/tests/integration/issues/1320.int.test.ts @@ -21,7 +21,6 @@ import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; import { gql } from "graphql-tag"; import type { Driver } from "neo4j-driver"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; @@ -62,11 +61,6 @@ describe("https://github.com/neo4j/graphql/issues/1320", () => { const neoGraphql = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret, - }), - }, }); schema = await neoGraphql.getSchema(); }); diff --git a/packages/graphql/tests/integration/issues/2889.int.test.ts b/packages/graphql/tests/integration/issues/2889.int.test.ts index 017dca058c..3d59ecd3b1 100644 --- a/packages/graphql/tests/integration/issues/2889.int.test.ts +++ b/packages/graphql/tests/integration/issues/2889.int.test.ts @@ -23,7 +23,6 @@ import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; import { cleanNodes } from "../../utils/clean-nodes"; -import { Neo4jGraphQLAuthJWTPlugin } from "../../../../plugins/graphql-plugin-auth/src"; describe("https://github.com/neo4j/graphql/issues/2889", () => { let driver: Driver; @@ -57,11 +56,6 @@ describe("https://github.com/neo4j/graphql/issues/2889", () => { neoSchema = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/issues/2981.int.test.ts b/packages/graphql/tests/integration/issues/2981.int.test.ts index 00903282c0..d38739da2e 100644 --- a/packages/graphql/tests/integration/issues/2981.int.test.ts +++ b/packages/graphql/tests/integration/issues/2981.int.test.ts @@ -23,7 +23,6 @@ import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; import { cleanNodes } from "../../utils/clean-nodes"; -import { Neo4jGraphQLAuthJWTPlugin } from "../../../../plugins/graphql-plugin-auth/src"; describe("https://github.com/neo4j/graphql/issues/2981", () => { let driver: Driver; @@ -70,11 +69,6 @@ describe("https://github.com/neo4j/graphql/issues/2981", () => { neoSchema = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/issues/3027.int.test.ts b/packages/graphql/tests/integration/issues/3027.int.test.ts index 2eb3830312..2bd0da69cf 100644 --- a/packages/graphql/tests/integration/issues/3027.int.test.ts +++ b/packages/graphql/tests/integration/issues/3027.int.test.ts @@ -23,7 +23,6 @@ import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; import { cleanNodes } from "../../utils/clean-nodes"; -import { Neo4jGraphQLAuthJWTPlugin } from "../../../../plugins/graphql-plugin-auth/src"; describe("https://github.com/neo4j/graphql/issues/3027", () => { describe("union", () => { @@ -71,11 +70,6 @@ describe("https://github.com/neo4j/graphql/issues/3027", () => { neoSchema = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); @@ -181,11 +175,6 @@ describe("https://github.com/neo4j/graphql/issues/3027", () => { neoSchema = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/integration/issues/3165.int.test.ts b/packages/graphql/tests/integration/issues/3165.int.test.ts index 391750e0cb..2daded83a6 100644 --- a/packages/graphql/tests/integration/issues/3165.int.test.ts +++ b/packages/graphql/tests/integration/issues/3165.int.test.ts @@ -23,7 +23,6 @@ import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; import { cleanNodes } from "../../utils/clean-nodes"; -import { Neo4jGraphQLAuthJWTPlugin } from "../../../../plugins/graphql-plugin-auth/src"; describe("https://github.com/neo4j/graphql/issues/3165", () => { let driver: Driver; @@ -75,11 +74,6 @@ describe("https://github.com/neo4j/graphql/issues/3165", () => { neoSchema = new Neo4jGraphQL({ typeDefs, driver, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "secret", - }), - }, }); }); diff --git a/packages/graphql/tests/tck/connections/projections/update.test.ts b/packages/graphql/tests/tck/connections/projections/update.test.ts index 3e11b8c4ae..e7e5011ef8 100644 --- a/packages/graphql/tests/tck/connections/projections/update.test.ts +++ b/packages/graphql/tests/tck/connections/projections/update.test.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import {} from "@neo4j/graphql-plugin-auth"; import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; import { Neo4jGraphQL } from "../../../../src"; diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json index 080e35d78f..f83aaf4654 100644 --- a/packages/graphql/tsconfig.json +++ b/packages/graphql/tsconfig.json @@ -4,13 +4,9 @@ "rootDir": ".", "baseUrl": ".", "outDir": "dist", - "paths": { - "@neo4j/graphql-plugin-auth": ["../plugins/graphql-plugin-auth/src"] - }, "noUncheckedIndexedAccess": true }, "include": ["global.d.ts", "package.json", "src/**/*", "tests/**/*"], - "references": [{ "path": "../plugins/graphql-plugin-auth/tsconfig.json" }], "ts-node": { "require": ["tsconfig-paths/register"] } diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 716a97f563..ddbe71c0f7 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -46,7 +46,6 @@ "neo4j-driver": "^4.1.0 || ^5.0.0" }, "devDependencies": { - "@neo4j/graphql-plugin-auth": "^2.2.0", "@types/jest": "29.5.3", "@types/node": "18.15.1", "camelcase": "6.3.0", diff --git a/packages/ogm/tsconfig.json b/packages/ogm/tsconfig.json index 400c93d7bb..47e608db32 100644 --- a/packages/ogm/tsconfig.json +++ b/packages/ogm/tsconfig.json @@ -5,10 +5,9 @@ "baseUrl": ".", "outDir": "dist", "paths": { - "@neo4j/graphql": ["../graphql/src"], - "@neo4j/graphql-plugin-auth": ["../plugins/graphql-plugin-auth/src"] + "@neo4j/graphql": ["../graphql/src"] } }, "include": ["src/**/*", "tests/**/*"], - "references": [{ "path": "../graphql/tsconfig.json" }, { "path": "../plugins/graphql-plugin-auth/tsconfig.json" }] + "references": [{ "path": "../graphql/tsconfig.json" }] } diff --git a/packages/plugins/graphql-plugin-auth/CHANGELOG.md b/packages/plugins/graphql-plugin-auth/CHANGELOG.md deleted file mode 100644 index 518b2f3513..0000000000 --- a/packages/plugins/graphql-plugin-auth/CHANGELOG.md +++ /dev/null @@ -1,32 +0,0 @@ -# @neo4j/graphql-plugin-auth - -## 2.2.0 - -### Minor Changes - -- [#3176](https://github.com/neo4j/graphql/pull/3176) [`4712a9a0a`](https://github.com/neo4j/graphql/commit/4712a9a0a741a456e660006c2e9c9b8bd1f869fb) Thanks [@tbwiss](https://github.com/tbwiss)! - feat: Auth plugins, constructor arguments for optional `iss` and `aud` JWT claim check on token verification - -## 2.1.0 - -### Minor Changes - -- [#2359](https://github.com/neo4j/graphql/pull/2359) [`3fd44b3ef`](https://github.com/neo4j/graphql/commit/3fd44b3ef08d6eebec3cb1dd51111af8bf4e9fb2) Thanks [@farhadnowzari](https://github.com/farhadnowzari)! - - The `JwksEndpoint` in `Neo4jGraphQLAuthJWKSPlugin` now will accept a function as well which returns a computed endpoint. - - The `Secret` in `Neo4jGraphQLAuthJWTPlugin` now will accept a function as well which returns a computed secret. - -## 2.0.0 - -### Major Changes - -- [#2710](https://github.com/neo4j/graphql/pull/2710) [`b081bec45`](https://github.com/neo4j/graphql/commit/b081bec4543abc3c66adf5588933632588cb0ce2) Thanks [@darrellwarde](https://github.com/darrellwarde)! - #2622 bumped the internal version of `jsonwebtoken`, which could cause some breaking changes in the authorization plugin. - -## 1.1.1 - -### Patch Changes - -- [#2515](https://github.com/neo4j/graphql/pull/2515) [`1bec3f95d`](https://github.com/neo4j/graphql/commit/1bec3f95d0f469c2a4e879b1904a4d1a4938207e) Thanks [@darrellwarde](https://github.com/darrellwarde)! - Add `bindPredicate` which allows the predicate used to evaluate `bind` rules to be changed - -## 1.1.0 - -### Minor Changes - -- [#1925](https://github.com/neo4j/graphql/pull/1925) [`1c589e246`](https://github.com/neo4j/graphql/commit/1c589e246f0ce9ffe82c5e7612deb4e7bac7c6e1) Thanks [@tbwiss](https://github.com/tbwiss)! - feat: Adding the functionality to enable global authentication via a setting in the Auth plugin diff --git a/packages/plugins/graphql-plugin-auth/LICENSE.txt b/packages/plugins/graphql-plugin-auth/LICENSE.txt deleted file mode 100644 index 93a20c352b..0000000000 --- a/packages/plugins/graphql-plugin-auth/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - https://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2022 Neo4j Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/packages/plugins/graphql-plugin-auth/README.md b/packages/plugins/graphql-plugin-auth/README.md deleted file mode 100644 index 55b571dd88..0000000000 --- a/packages/plugins/graphql-plugin-auth/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# @neo4j/graphql-plugin-auth - -Auth decode plugins for @neo4j/graphql - -1. [Documentation](https://neo4j.com/docs/graphql-manual/current/auth/) - -## Installation - -``` -$ npm install @neo4j/graphql-plugin-auth -``` - -## Usage - -### `Neo4jGraphQLAuthJWTPlugin` - -```ts -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWTPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: "super-secret", - }), - }, -}); - -// Or you can initiate the secret with a function which will run to retrieve the secret when the request comes in - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWTPlugin({ - secret: (req) => { - return "super-secret"; - }, - }), - }, -}); -``` - -### `Neo4jGraphQLAuthJWKSPlugin` - -```ts -import { Neo4jGraphQL } from "@neo4j/graphql"; -import { Neo4jGraphQLAuthJWKSPlugin } from "@neo4j/graphql-plugin-auth"; - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json", - }), - }, -}); - -//Or you can pass a function as jskwsEndpoint to compute the endpoint when the request comes in. - -const neoSchema = new Neo4jGraphQL({ - typeDefs, - plugins: { - auth: new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: (req) => { - let url = "https://YOUR_DOMAIN/well-known/{file}.json"; - const fileHeader = req.headers["file"]; - url = url.replace("{file}", fileHeader); - return url; - }, - }), - }, -}); -``` - -## Licence - -[Apache 2.0](https://github.com/neo4j/graphql/blob/master/packages/graphql-plugin-auth/LICENSE.txt) diff --git a/packages/plugins/graphql-plugin-auth/jest.config.js b/packages/plugins/graphql-plugin-auth/jest.config.js deleted file mode 100644 index e801bcd798..0000000000 --- a/packages/plugins/graphql-plugin-auth/jest.config.js +++ /dev/null @@ -1,16 +0,0 @@ -const globalConf = require("../../../jest.config.base"); - -module.exports = { - ...globalConf, - displayName: "@neo4j/graphql-plugin-auth", - roots: ["/packages/plugins/graphql-plugin-auth/src"], - coverageDirectory: "/packages/plugins/graphql-plugin-auth/coverage/", - transform: { - "^.+\\.ts$": [ - "ts-jest", - { - tsconfig: "/packages/plugins/graphql-plugin-auth/tsconfig.json", - }, - ], - }, -}; diff --git a/packages/plugins/graphql-plugin-auth/package.json b/packages/plugins/graphql-plugin-auth/package.json deleted file mode 100644 index b1500a18aa..0000000000 --- a/packages/plugins/graphql-plugin-auth/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "@neo4j/graphql-plugin-auth", - "version": "2.2.0", - "description": "Auth decode plugins for @neo4j/graphql", - "keywords": [ - "neo4j", - "graphql", - "auth" - ], - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/neo4j/graphql/issues" - }, - "homepage": "https://github.com/neo4j/graphql/tree/dev/packages/plugins/graphql-plugin-auth", - "exports": "./dist/index.js", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist/**/*.ts", - "dist/**/*.ts.map", - "dist/**/*.js", - "dist/**/*.js.map" - ], - "scripts": { - "build": "tsc --build tsconfig.production.json", - "test": "jest", - "test:unit": "jest src/**/*.test.ts", - "test:unit:watch": "jest src --watch" - }, - "author": "Neo4j Inc.", - "dependencies": { - "debug": "^4.3.4", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.0.0" - }, - "devDependencies": { - "@types/debug": "4.1.8", - "@types/jest": "29.5.3", - "@types/jsonwebtoken": "9.0.2", - "@types/node": "18.15.1", - "jest": "29.6.1", - "ts-jest": "29.1.1", - "typescript": "5.1.6" - } -} diff --git a/packages/plugins/graphql-plugin-auth/sonar-project.properties b/packages/plugins/graphql-plugin-auth/sonar-project.properties deleted file mode 100644 index a44a9b1a9d..0000000000 --- a/packages/plugins/graphql-plugin-auth/sonar-project.properties +++ /dev/null @@ -1,12 +0,0 @@ -sonar.projectKey=neo4j-organization-graphql-plugin-auth -sonar.organization=neo4j-organization - -# This is the name and version displayed in the SonarCloud UI. -#sonar.projectName=neo4j-organization-graphql-plugin-auth -#sonar.projectVersion=1.0 - -# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. -#sonar.sources=. - -# Encoding of the source code. Default is default system encoding -#sonar.sourceEncoding=UTF-8 diff --git a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.test.ts b/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.test.ts deleted file mode 100644 index f379b26c2a..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Neo4jGraphQLAuthJWKSPlugin from "./Neo4jGraphQLAuthJWKSPlugin"; - -describe("Neo4jGraphQLAuthJWKSPlugin", () => { - test("should construct", () => { - const plugin = new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: "endpoint.com", - }); - - expect(plugin).toBeInstanceOf(Neo4jGraphQLAuthJWKSPlugin); - expect(plugin.client).not.toBeNull(); - }); - test("client should be null when jwksEndpoint is a function", () => { - const plugin = new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: () => { - return "https://my-dummy-identity:8080/tenant1"; - }, - }); - expect(plugin.client).toBeNull(); - }); - test("tryToResolveKeys should run the jwksEndpoint as function", () => { - const expectedEndpoint = "https://my-dummy-identity:8080/tenant1"; - const plugin = new Neo4jGraphQLAuthJWKSPlugin({ - jwksEndpoint: () => { - return expectedEndpoint; - }, - }); - //How we use the headers or request inside the jwksFunction is not the logic of `tryToResolveKeys` method - plugin.tryToResolveKeys({ - headers: { - test: "test_header", - }, - }); - - expect(plugin.options.jwksUri).toBe(expectedEndpoint); - expect(plugin.client).not.toBeNull(); - }); -}); diff --git a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.ts b/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.ts deleted file mode 100644 index ebcdc8009d..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWKSPlugin.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import jsonwebtoken from "jsonwebtoken"; -import type JwksRsa from "jwks-rsa"; -import { JwksClient } from "jwks-rsa"; -import Debug from "debug"; -import { DEBUG_PREFIX } from "./constants"; -import type { RequestLike } from "./types"; - -const debug = Debug(DEBUG_PREFIX); - -export interface JWKSPluginInput { - jwksEndpoint: string | ((req: RequestLike) => string); - rolesPath?: string; - globalAuthentication?: boolean; - bindPredicate?: "all" | "any"; - issuer?: string | string[]; - audience?: string | RegExp | (string | RegExp)[]; -} - -class Neo4jGraphQLAuthJWKSPlugin { - rolesPath?: string; - isGlobalAuthenticationEnabled?: boolean; - client: JwksClient | null = null; - options!: JwksRsa.Options; - bindPredicate: "all" | "any"; - input: JWKSPluginInput; - - constructor(input: JWKSPluginInput) { - //We are going to use this input later, so we need to save it here. - this.input = input; - - this.rolesPath = input.rolesPath; - this.isGlobalAuthenticationEnabled = input.globalAuthentication || false; - this.bindPredicate = input.bindPredicate || "all"; - - //It will be empty string if the endpoint is a function - //This means the value will be calculated later - const jwksEndpoint = typeof input.jwksEndpoint === "string" ? input.jwksEndpoint : ""; - - this.options = { - jwksUri: jwksEndpoint, - rateLimit: true, - jwksRequestsPerMinute: 10, - cache: true, - cacheMaxEntries: 5, - cacheMaxAge: 600000, - }; - - //If the endpoint is set in the constructor directly we can create th client immediately here - if (jwksEndpoint !== "") this.client = new JwksClient(this.options); - } - - tryToResolveKeys(req: RequestLike): void { - if (typeof this.input.jwksEndpoint === "string") return; - - //The url will be computed based on the jwksEndpoint implementation - this.options.jwksUri = this.input.jwksEndpoint(req); - - this.client = new JwksClient(this.options); - - return; - } - - async decode(token: string): Promise { - let result: T | undefined; - - try { - debug("Verifying JWT using OpenID Public Key Set Endpoint"); - - result = await this.verifyJWKS({ - token, - }); - } catch (error) { - debug("%s", error); - } - - return result; - } - - private async verifyJWKS({ token }: { token: string }): Promise { - const getKey: jsonwebtoken.GetPublicKeyOrSecret = (header, callback) => { - if (!this.client) { - debug( - "JwksClient should NOT be empty! Make sure the 'tryToResolveKeys' method is called before decoding" - ); - return; - } - const kid: string = header.kid || ""; - - this.client.getSigningKey(kid, (err, key) => { - const signingKey = key?.getPublicKey(); - callback(err, signingKey); - }); - }; - - return new Promise((resolve, reject) => { - if (!this.client) - reject( - "JwksClient should not be empty! Make sure the 'tryToResolveKeys' method is called before decoding" - ); - jsonwebtoken.verify( - token, - getKey, - { - algorithms: ["HS256", "RS256"], - issuer: this.input.issuer, - audience: this.input.audience, - }, - (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as unknown as T); - } - } - ); - }); - } -} - -export default Neo4jGraphQLAuthJWKSPlugin; diff --git a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.test.ts b/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.test.ts deleted file mode 100644 index efbf27e609..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION } from "./exceptions"; -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as jsonwebtoken from "jsonwebtoken"; -import Neo4jGraphQLAuthJWTPlugin from "./Neo4jGraphQLAuthJWTPlugin"; - -describe("Neo4jGraphQLAuthJWTPlugin", () => { - const secret = "secret"; - const payload = { - sub: "my-id", - }; - test("should decode token", async () => { - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test("should decode token when secret is a generic", async () => { - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret: () => { - return secret; - }, - }); - - plugin.tryToResolveKeys({ - headers: { - test: "test-header", - }, - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test(`should throw '${AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION}' exception, when a function for calculating the secret is passed but the result is null`, async () => { - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret: () => { - return secret; - }, - }); - - await expect(async () => { - await plugin.decode(encoded); - }).rejects.toEqual(AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION); - }); - - test("should decode token when using noVerify", async () => { - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - noVerify: true, - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test("should decode JWT token with globalAuthentication enabled", async () => { - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - globalAuthentication: true, - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test("should throw an error if both noVerify and globalAuthentication are enabled", async () => { - let initError: unknown = null; - try { - const payload = { - sub: "my-id", - }; - - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - noVerify: true, - globalAuthentication: true, - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - } catch (error) { - initError = error; - } - - expect(initError).toBeDefined(); - expect((initError as Error).message).toBe( - "Neo4jGraphQLAuthJWTPlugin, noVerify and globalAuthentication can not both be enabled simultaneously." - ); - }); - - test("should decode token and verify issuer", async () => { - const payload = { - sub: "my-id", - iss: "https://testcompany.com", - }; - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - issuer: "https://testcompany.com", - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test("decode function should return undefined if token contains a different issuer than specified in Neo4jGraphQLAuthJWTPlugin", async () => { - const payload = { - sub: "my-id", - iss: "https://testcompany.com", - }; - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - issuer: "https://anothercomapny.com", - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toBeUndefined(); - }); - - test("should decode token and verify audience", async () => { - const payload = { - sub: "my-id", - aud: "urn:user", - }; - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - audience: "urn:user", - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toMatchObject(payload); - }); - - test("decode function should return undefined if token contains a different audience than specified in Neo4jGraphQLAuthJWTPlugin", async () => { - const payload = { - sub: "my-id", - aud: "urn:user", - }; - const encoded = jsonwebtoken.sign(payload, secret); - - const plugin = new Neo4jGraphQLAuthJWTPlugin({ - secret, - audience: "urn:otheruser", - }); - - const decoded = await plugin.decode(encoded); - - expect(decoded).toBeUndefined(); - }); -}); diff --git a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.ts b/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.ts deleted file mode 100644 index b4c10e602c..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/Neo4jGraphQLAuthJWTPlugin.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import jsonwebtoken from "jsonwebtoken"; -import Debug from "debug"; -import { DEBUG_PREFIX } from "./constants"; -import type { RequestLike } from "./types"; -import { AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION } from "./exceptions"; - -const debug = Debug(DEBUG_PREFIX); - -export interface JWTPluginInput { - secret: jsonwebtoken.Secret | ((req: RequestLike) => jsonwebtoken.Secret); - noVerify?: boolean; - globalAuthentication?: boolean; - rolesPath?: string; - bindPredicate?: "all" | "any"; - issuer?: string | string[]; - audience?: string | RegExp | (string | RegExp)[]; -} - -class Neo4jGraphQLAuthJWTPlugin { - private secret: jsonwebtoken.Secret | null = null; - private noVerify?: boolean; - rolesPath?: string; - isGlobalAuthenticationEnabled?: boolean; - input: JWTPluginInput; - bindPredicate: "all" | "any"; - - constructor(input: JWTPluginInput) { - this.input = input; - - this.secret = typeof input.secret === "function" ? null : input.secret; - this.noVerify = input.noVerify; - this.rolesPath = input.rolesPath; - this.isGlobalAuthenticationEnabled = input.globalAuthentication || false; - this.bindPredicate = input.bindPredicate || "all"; - - if (this.noVerify && this.isGlobalAuthenticationEnabled) { - throw new Error( - "Neo4jGraphQLAuthJWTPlugin, noVerify and globalAuthentication can not both be enabled simultaneously." - ); - } - } - - tryToResolveKeys(req: RequestLike): void { - if (typeof this.input.secret !== "function") return; - - this.secret = this.input.secret(req); - - return; - } - - /* eslint-disable @typescript-eslint/require-await */ - async decode(token: string): Promise { - let result: T | undefined; - - try { - if (this.noVerify) { - debug("Skipping verifying JWT as noVerify is not set"); - - result = jsonwebtoken.decode(token, { json: true }) as unknown as T; - } - if (this.secret) { - debug("Verifying JWT using secret"); - - result = jsonwebtoken.verify(token, this.secret, { - algorithms: ["HS256", "RS256"], - issuer: this.input.issuer, - audience: this.input.audience, - }) as unknown as T; - } else if (typeof this.input.secret === "function" && !this.secret) { - debug("'secret' should not be null, make sure the 'tryToResolveKeys' is ran before the decode method."); - throw AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION; - } - } catch (error) { - debug("%s", error); - if (error === AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION) { - throw error; - } - } - - return result; - } - /* eslint-enable @typescript-eslint/require-await */ -} - -export default Neo4jGraphQLAuthJWTPlugin; diff --git a/packages/plugins/graphql-plugin-auth/src/constants.ts b/packages/plugins/graphql-plugin-auth/src/constants.ts deleted file mode 100644 index b4e132f2ea..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const DEBUG_PREFIX = "@neo4j/graphql-plugin-auth"; diff --git a/packages/plugins/graphql-plugin-auth/src/exceptions.ts b/packages/plugins/graphql-plugin-auth/src/exceptions.ts deleted file mode 100644 index 269f2951a7..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/exceptions.ts +++ /dev/null @@ -1 +0,0 @@ -export const AUTH_JWT_PLUGIN_NULL_SECRET_EXCEPTION = "Secret cannot be null"; diff --git a/packages/plugins/graphql-plugin-auth/src/index.test.ts b/packages/plugins/graphql-plugin-auth/src/index.test.ts deleted file mode 100644 index 9dc2923a18..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/index.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQLAuthJWTPlugin, Neo4jGraphQLAuthJWKSPlugin } from "."; - -describe("index", () => { - test("Neo4jGraphQLAuthJWTPlugin", () => { - expect(Neo4jGraphQLAuthJWTPlugin).toBeDefined(); - }); - - test("Neo4jGraphQLAuthJWKSPlugin", () => { - expect(Neo4jGraphQLAuthJWKSPlugin).toBeDefined(); - }); -}); diff --git a/packages/plugins/graphql-plugin-auth/src/index.ts b/packages/plugins/graphql-plugin-auth/src/index.ts deleted file mode 100644 index d28ea0d671..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { default as Neo4jGraphQLAuthJWTPlugin } from "./Neo4jGraphQLAuthJWTPlugin"; -export { default as Neo4jGraphQLAuthJWKSPlugin } from "./Neo4jGraphQLAuthJWKSPlugin"; diff --git a/packages/plugins/graphql-plugin-auth/src/types.ts b/packages/plugins/graphql-plugin-auth/src/types.ts deleted file mode 100644 index 3efd71febd..0000000000 --- a/packages/plugins/graphql-plugin-auth/src/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -type RequestLike = { - headers?: { [key: string]: string }; - cookies?: { token?: string }; -}; - -export { RequestLike }; diff --git a/packages/plugins/graphql-plugin-auth/tsconfig.json b/packages/plugins/graphql-plugin-auth/tsconfig.json deleted file mode 100644 index bdba6fa01a..0000000000 --- a/packages/plugins/graphql-plugin-auth/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": ".", - "baseUrl": ".", - "outDir": "dist" - }, - "include": ["src/**/*"] -} diff --git a/packages/plugins/graphql-plugin-auth/tsconfig.production.json b/packages/plugins/graphql-plugin-auth/tsconfig.production.json deleted file mode 100644 index 65c1e3045e..0000000000 --- a/packages/plugins/graphql-plugin-auth/tsconfig.production.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "src" - }, - "exclude": ["src/**/*.test.ts"], - "include": ["src/**/*"] -} diff --git a/packages/plugins/graphql-plugin-subscriptions-amqp/README.md b/packages/plugins/graphql-plugin-subscriptions-amqp/README.md index cce236cb5e..669213947e 100644 --- a/packages/plugins/graphql-plugin-subscriptions-amqp/README.md +++ b/packages/plugins/graphql-plugin-subscriptions-amqp/README.md @@ -2,9 +2,9 @@ Subscription plugin for `@neo4j/graphql`, currently supporting AMQP 0-9-1 brokers such as: -- RabbitMQ -- Apache Qpid -- Apache ActiveMQ +* RabbitMQ +* Apache Qpid +* Apache ActiveMQ [Documentation](https://neo4j.com/docs/graphql-manual/current/subscriptions/) @@ -46,19 +46,15 @@ await plugin.close(); The following options are available in the plugin. -- **connection**: [AMQP](https://www.npmjs.com/package/amqplib) connection options or amqp url (e.g. `amqp://localhost`). -- **exchange**: (optional) Queue exchange, defaults to `neo4j.graphql.subscriptions.fx`. -- **reconnectTimeout**: (optional) Timeout (in ms) between reconnection attempts. If not set, the plugin will not reconnect. Note that if the first connection fails, it will not attempt to reconnect. -- **log**: (optional) Enable AMQP logs, defaults to `true`. -- **amqpVersion**: (optional) AMQP version to use, only `0-9-1` supported at the moment. +* **connection**: [AMQP](https://www.npmjs.com/package/amqplib) connection options or amqp url (e.g. `amqp://localhost`). +* **exchange**: (optional) Queue exchange, defaults to `neo4j.graphql.subscriptions.fx`. +* **reconnectTimeout**: (optional) Timeout (in ms) between reconnection attempts. If not set, the plugin will not reconnect. Note that if the first connection fails, it will not attempt to reconnect. +* **log**: (optional) Enable AMQP logs, defaults to `true`. +* **amqpVersion**: (optional) AMQP version to use, only `0-9-1` supported at the moment. ## Running tests -- `yarn test` to run unit tests -- `yarn test:e2e` to run integration tests. These tests require a RabbitMQ instance running, and are not run by default - - Use `docker-compose up rabbitmq` to spin up a RabbitMQ container for testing - - Use `docker-compose up qpid` to spin up a Qpid container for testing - -## Licence - -[Apache 2.0](https://github.com/neo4j/graphql/blob/master/packages/graphql-plugin-auth/LICENSE.txt) +* `yarn test` to run unit tests +* `yarn test:e2e` to run integration tests. These tests require a RabbitMQ instance running, and are not run by default + * Use `docker-compose up rabbitmq` to spin up a RabbitMQ container for testing + * Use `docker-compose up qpid` to spin up a Qpid container for testing diff --git a/tsconfig.json b/tsconfig.json index 7b467c534a..47d0955883 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,6 @@ "references": [ { "path": "./packages/graphql/tsconfig.json" }, { "path": "./packages/ogm/tsconfig.json" }, - { "path": "./packages/introspector/tsconfig.json" }, - { "path": "./packages/plugins/graphql-plugin-auth/tsconfig.json" } + { "path": "./packages/introspector/tsconfig.json" } ] } diff --git a/tsconfig.production.json b/tsconfig.production.json index f0284a50b0..4b4cf477e1 100644 --- a/tsconfig.production.json +++ b/tsconfig.production.json @@ -4,7 +4,6 @@ { "path": "./packages/graphql/src/tsconfig.production.json" }, { "path": "./packages/ogm/tsconfig.production.json" }, { "path": "./packages/introspector/tsconfig.production.json" }, - { "path": "./packages/plugins/graphql-plugin-auth/tsconfig.production.json" }, { "path": "./packages/plugins/graphql-plugin-subscriptions-amqp/tsconfig.production.json" } ] } diff --git a/yarn.lock b/yarn.lock index 17d13117f8..c248183a95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3751,7 +3751,6 @@ __metadata: "@graphql-codegen/typescript": ^4.0.0 "@graphql-tools/merge": ^9.0.0 "@neo4j/graphql": ^3.23.1 - "@neo4j/graphql-plugin-auth": ^2.2.0 "@types/jest": 29.5.3 "@types/node": 18.15.1 camelcase: 6.3.0 @@ -3772,23 +3771,6 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql-plugin-auth@^2.1.0, @neo4j/graphql-plugin-auth@^2.2.0, @neo4j/graphql-plugin-auth@workspace:packages/plugins/graphql-plugin-auth": - version: 0.0.0-use.local - resolution: "@neo4j/graphql-plugin-auth@workspace:packages/plugins/graphql-plugin-auth" - dependencies: - "@types/debug": 4.1.8 - "@types/jest": 29.5.3 - "@types/jsonwebtoken": 9.0.2 - "@types/node": 18.15.1 - debug: ^4.3.4 - jest: 29.6.1 - jsonwebtoken: ^9.0.0 - jwks-rsa: ^3.0.0 - ts-jest: 29.1.1 - typescript: 5.1.6 - languageName: unknown - linkType: soft - "@neo4j/graphql-plugin-subscriptions-amqp@^2.0.0, @neo4j/graphql-plugin-subscriptions-amqp@workspace:packages/plugins/graphql-plugin-subscriptions-amqp": version: 0.0.0-use.local resolution: "@neo4j/graphql-plugin-subscriptions-amqp@workspace:packages/plugins/graphql-plugin-subscriptions-amqp" @@ -3903,7 +3885,6 @@ __metadata: "@graphql-tools/schema": 10.0.0 "@graphql-tools/utils": ^10.0.0 "@neo4j/cypher-builder": ^1.0.0 - "@neo4j/graphql-plugin-auth": ^2.2.0 "@types/deep-equal": 1.0.1 "@types/is-uuid": 1.0.0 "@types/jest": 29.5.3 @@ -3929,6 +3910,7 @@ __metadata: jest: 29.6.1 jest-extended: 4.0.0 jose: ^4.13.1 + jwks-rsa: ^3.0.1 koa: 2.14.2 koa-jwt: 4.0.4 koa-router: 12.0.0 @@ -18598,7 +18580,7 @@ __metadata: languageName: node linkType: hard -"jwks-rsa@npm:^3.0.0": +"jwks-rsa@npm:^3.0.1": version: 3.0.1 resolution: "jwks-rsa@npm:3.0.1" dependencies: @@ -20716,7 +20698,6 @@ __metadata: dependencies: "@apollo/server": ^4.7.0 "@neo4j/graphql": ^3.15.0 - "@neo4j/graphql-plugin-auth": ^2.1.0 "@neo4j/graphql-plugin-subscriptions-amqp": ^2.0.0 concurrently: 8.2.0 dotenv: ^16.0.3