From 008633f2ff52c36a4bce7c86a203e1527ad628a1 Mon Sep 17 00:00:00 2001 From: Dylan Vanmali Date: Thu, 4 Nov 2021 16:47:27 -0700 Subject: [PATCH 1/3] Added OpenID Json Web Key Sets for verification via the @auth directive --- docs/modules/ROOT/pages/auth/setup.adoc | 29 +++++- packages/graphql/package.json | 1 + packages/graphql/src/auth/get-jwt.ts | 31 ++++++- packages/graphql/src/classes/Neo4jGraphQL.ts | 3 +- yarn.lock | 97 ++++++++++++++++++++ 5 files changed, 153 insertions(+), 8 deletions(-) diff --git a/docs/modules/ROOT/pages/auth/setup.adoc b/docs/modules/ROOT/pages/auth/setup.adoc index 13b3b7b79f..7f34ed3e10 100644 --- a/docs/modules/ROOT/pages/auth/setup.adoc +++ b/docs/modules/ROOT/pages/auth/setup.adoc @@ -3,13 +3,31 @@ == Configuration -If you want the Neo4j GraphQL Library to perform JWT decoding and verification for you, you must pass the configuration option `jwt` into the `Neo4jGraphQL` or `OGM` constructor, which has the following arguments: +If you want the Neo4j GraphQL Library to perform JWT decoding and verification for you, you must pass the configuration option `jwt` into the `Neo4jGraphQL` or `OGM` constructor, which has EITHER of the following arguments: + +- `jwksEndpoint` (recomended) - The OpenID Public Key Endpoint that verifies the JWT. The https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-sets[JSON Web Key Sets] used under the OpenID Configuration is typically stored at an endpoint similar to \https://YOUR_DOMAIN/.well-known/jwks.json +- `secret` - The secret to be used to decode and verify JWTs. Either the secret or the PEM encoded public key used to verify the JWT. + +Optional arguments: -- `secret` - The secret to be used to decode and verify JWTs - `noVerify` (optional) - Disable verification of JWTs, defaults to _false_ - `rolesPath` (optional) - A string key to specify where to find roles in the JWT, defaults to "roles" -The simplest construction of a `Neo4jGraphQL` instance would be: +The recommended construction of a `Neo4jGraphQL` instance would be: + +[source, javascript, indent=0] +---- +const neoSchema = new Neo4jGraphQL({ + typeDefs, + config: { + jwt: { + jwksEndpoint: "https://YOUR_DOMAIN/.well-known/jwks.json" + } + } +}); +---- + +Another valid construction could be: [source, javascript, indent=0] ---- @@ -23,6 +41,7 @@ const neoSchema = new Neo4jGraphQL({ }); ---- + 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/setup.adoc#auth-setup-passing-in[Passing in JWTs] below. === Auth Roles Object Paths @@ -46,8 +65,8 @@ const neoSchema = new Neo4jGraphQL({ typeDefs, config: { jwt: { - secret, - rolesPath: "https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles" + jwksEndpoint: "https://YOUR_DOMAIN/.well-known/jwks.json", + rolesPath: "https://YOUR_DOMAIN/claims\\.https://YOUR_DOMAIN/claims/roles" } } }); diff --git a/packages/graphql/package.json b/packages/graphql/package.json index a003bd68b4..2b19f01e35 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -72,6 +72,7 @@ "graphql-parse-resolve-info": "^4.12.0", "graphql-relay": "^0.8.0", "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^2.0.5", "pluralize": "^8.0.0", "semver": "^7.3.5" }, diff --git a/packages/graphql/src/auth/get-jwt.ts b/packages/graphql/src/auth/get-jwt.ts index 721aa4f3b5..c66589c75a 100644 --- a/packages/graphql/src/auth/get-jwt.ts +++ b/packages/graphql/src/auth/get-jwt.ts @@ -19,6 +19,7 @@ import { IncomingMessage } from "http"; import jsonwebtoken from "jsonwebtoken"; +import { JwksClient } from "jwks-rsa"; import Debug from "debug"; import { Context } from "../types"; import { DEBUG_AUTH } from "../constants"; @@ -28,6 +29,7 @@ const debug = Debug(DEBUG_AUTH); function getJWT(context: Context): any { const jwtConfig = context.neoSchema.config?.jwt; let result; + let client; if (!jwtConfig) { debug("JWT not configured"); @@ -68,8 +70,33 @@ function getJWT(context: Context): any { debug("Skipping verifying JWT as noVerify is not set"); result = jsonwebtoken.decode(token); - } else { - debug("Verifying JWT"); + } else if (jwtConfig.jwkEndpoint) { + debug("Verifying JWT using OpenID Public Key Endpoint"); + + // Create a JWK Client with a rate limit that + // limits the number of calls to our JWK endpoint + client = new JwksClient({ + jwksUri: jwtConfig.jwkEndpoint || "", + rateLimit: true, + jwksRequestsPerMinute: 10, // Default Value + cache: true, // Default Value + cacheMaxEntries: 5, // Default value + cacheMaxAge: 600000, // Defaults to 10m + }); + + /* eslint-disable-next-line no-inner-declarations */ + function getKey(header, callback) { + client.getSigningKey(header.kid, function (err, key) { + const signingKey = key.getPublicKey(); + callback(null, signingKey); + }); + } + + result = jsonwebtoken.verify(token, getKey, { + algorithms: ["HS256", "RS256"], + }); + } else if (jwtConfig.secret) { + debug("Verifying JWT using secret"); result = jsonwebtoken.verify(token, jwtConfig.secret, { algorithms: ["HS256", "RS256"], diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 821c48dec6..1463a80d80 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -38,7 +38,8 @@ import assertIndexesAndConstraints, { const debug = Debug(DEBUG_GRAPHQL); export interface Neo4jGraphQLJWT { - secret: string; + jwkEndpoint?: string; + secret?: string; noVerify?: boolean; rolesPath?: string; } diff --git a/yarn.lock b/yarn.lock index b14d00899d..de35360d07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1071,6 +1071,7 @@ __metadata: is-uuid: 1.0.2 jest: 26.2.2 jsonwebtoken: ^8.5.1 + jwks-rsa: ^2.0.5 libnpmsearch: 3.1.0 npm-run-all: 4.1.5 pluralize: ^8.0.0 @@ -1140,6 +1141,13 @@ __metadata: languageName: node linkType: hard +"@panva/asn1.js@npm:^1.0.0": + version: 1.0.0 + resolution: "@panva/asn1.js@npm:1.0.0" + checksum: 0563c1372d99051d8e2268541a6ef9ec5d0495e9b943b0b1b738eb40ffd21e8bf7690f313d5ce86cccb32e646f9bb75f1af3cd45bce5d04b85ad9c04f0b596aa + languageName: node + linkType: hard + "@popperjs/core@npm:^2.5.3": version: 2.9.2 resolution: "@popperjs/core@npm:2.9.2" @@ -1511,6 +1519,16 @@ __metadata: languageName: node linkType: hard +"@types/express-jwt@npm:0.0.42": + version: 0.0.42 + resolution: "@types/express-jwt@npm:0.0.42" + dependencies: + "@types/express": "*" + "@types/express-unless": "*" + checksum: 82477e875e2ef225c69488262bbf86d77c3ce604792b3d8d9fcf4033296fc66baa290f37aed05e237cee8e2bbd6f801f0f75d5b661f649c94a668f20b66899f8 + languageName: node + linkType: hard + "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:4.17.19, @types/express-serve-static-core@npm:^4.17.18": version: 4.17.19 resolution: "@types/express-serve-static-core@npm:4.17.19" @@ -1544,6 +1562,15 @@ __metadata: languageName: node linkType: hard +"@types/express-unless@npm:*": + version: 0.5.2 + resolution: "@types/express-unless@npm:0.5.2" + dependencies: + "@types/express": "*" + checksum: 113ca55a96f4aa21e0ddc4e8b95efe27e55213a7dfe0d3cce87330e32742edbbe3c3249e61e7b93a2cf584ba3227f14c7bea7b64d6eef17f4fdab6b268cd0cb4 + languageName: node + linkType: hard + "@types/express@npm:*, @types/express@npm:4.17.11": version: 4.17.11 resolution: "@types/express@npm:4.17.11" @@ -8813,6 +8840,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +"jose@npm:^2.0.5": + version: 2.0.5 + resolution: "jose@npm:2.0.5" + dependencies: + "@panva/asn1.js": ^1.0.0 + checksum: 7c6eecb0bf26109c1be5a18084276492a957be4876acae2529904c4e54cee50c736a8fe65702bcdd7537313a3a9ff94e44f9987423a59989920018ff144eb936 + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -9023,6 +9059,19 @@ fsevents@^1.2.7: languageName: node linkType: hard +"jwks-rsa@npm:^2.0.5": + version: 2.0.5 + resolution: "jwks-rsa@npm:2.0.5" + dependencies: + "@types/express-jwt": 0.0.42 + debug: ^4.3.2 + jose: ^2.0.5 + limiter: ^1.1.5 + lru-memoizer: ^2.1.4 + checksum: 9bb9f9be0c507e448af95a57ae6caf0e63defd3b935c2bb98b5b2fd9f43e23ff84b45e643a30f04118b7b9e88fefd8b7ec449b6e4af2f2bf3d0e906a23096012 + languageName: node + linkType: hard + "jws@npm:^3.2.2": version: 3.2.2 resolution: "jws@npm:3.2.2" @@ -9149,6 +9198,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"limiter@npm:^1.1.5": + version: 1.1.5 + resolution: "limiter@npm:1.1.5" + checksum: 83f7aa20fefdcfc72a76f034ce804ad8d0063cd94c3ebd9c9b08157051b3f76b099cb953ccd09a0117ed06b876773c6b3876afd88be8d11ced252a8a85603786 + languageName: node + linkType: hard + "lines-and-columns@npm:^1.1.6": version: 1.1.6 resolution: "lines-and-columns@npm:1.1.6" @@ -9304,6 +9360,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"lodash.clonedeep@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeep@npm:4.5.0" + checksum: 41e2fe4c57c56a66a4775a6ddeebe9272f0ce4d257d97b3cb8724a9b01eeec9b09ce7e8603d6926baf5f48c287d988f0de4bf5aa244ea86b1f22c1e6f203cc27 + languageName: node + linkType: hard + "lodash.includes@npm:^4.3.0": version: 4.3.0 resolution: "lodash.includes@npm:4.3.0" @@ -9453,6 +9516,26 @@ fsevents@^1.2.7: languageName: node linkType: hard +"lru-cache@npm:~4.0.0": + version: 4.0.2 + resolution: "lru-cache@npm:4.0.2" + dependencies: + pseudomap: ^1.0.1 + yallist: ^2.0.0 + checksum: 503a21816bef4909ebb3a6b6916f8aafb73c850cf4dd27905d2971311954d630ec004c72ac8521165cf196782f8a45b13cc34935209cec6339e4951e155e659b + languageName: node + linkType: hard + +"lru-memoizer@npm:^2.1.4": + version: 2.1.4 + resolution: "lru-memoizer@npm:2.1.4" + dependencies: + lodash.clonedeep: ^4.5.0 + lru-cache: ~4.0.0 + checksum: 31f52aaec58f3185f256d78fb803398c11727adf5fb318fedc11cf77a4297815f409d36e509dad788cd2e19254756599f36ef40d0ec91db83320bc5ebcc91da3 + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -11310,6 +11393,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"pseudomap@npm:^1.0.1": + version: 1.0.2 + resolution: "pseudomap@npm:1.0.2" + checksum: 1ad1802645e830d99f9c1db97efc6902d2316b660454633229f636dd59e751d00498dd325d3b18d49f2be990a2c9d28f8bfe6f9b544a8220a5faa2bfb4694bb7 + languageName: node + linkType: hard + "psl@npm:^1.1.28, psl@npm:^1.1.33": version: 1.8.0 resolution: "psl@npm:1.8.0" @@ -14705,6 +14795,13 @@ typescript@4.1.3: languageName: node linkType: hard +"yallist@npm:^2.0.0": + version: 2.1.2 + resolution: "yallist@npm:2.1.2" + checksum: f83e3d18eeba68a0276be2ab09260be3f2a300307e84b1565c620ef71f03f106c3df9bec4c3a91e5fa621a038f8826c19b3786804d3795dd4f999e5b6be66ea3 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" From ed2bdcece686c13deeb66b7ab006a6acbed246e5 Mon Sep 17 00:00:00 2001 From: Dylan Vanmali Date: Fri, 5 Nov 2021 11:43:28 -0700 Subject: [PATCH 2/3] Remove unnecessary quote when using jwtConfig The value is already set so there is no need of a default value "" --- packages/graphql/src/auth/get-jwt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/auth/get-jwt.ts b/packages/graphql/src/auth/get-jwt.ts index c66589c75a..69850ed38e 100644 --- a/packages/graphql/src/auth/get-jwt.ts +++ b/packages/graphql/src/auth/get-jwt.ts @@ -76,7 +76,7 @@ function getJWT(context: Context): any { // Create a JWK Client with a rate limit that // limits the number of calls to our JWK endpoint client = new JwksClient({ - jwksUri: jwtConfig.jwkEndpoint || "", + jwksUri: jwtConfig.jwkEndpoint, rateLimit: true, jwksRequestsPerMinute: 10, // Default Value cache: true, // Default Value From f4792611be111d3b64a52e1bea8f8e579efd1c77 Mon Sep 17 00:00:00 2001 From: Dylan Vanmali Date: Fri, 5 Nov 2021 12:05:31 -0700 Subject: [PATCH 3/3] git command to add remote repo was incorrect --- docs/markdown/DEVELOPING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/markdown/DEVELOPING.md b/docs/markdown/DEVELOPING.md index 09a3d7dfe6..7b1e03ffd4 100644 --- a/docs/markdown/DEVELOPING.md +++ b/docs/markdown/DEVELOPING.md @@ -31,7 +31,7 @@ git@github.com:USERNAME/graphql.git You will then need to add our repository as an upstream: ```bash -git add remote upstream git@github.com/neo4j/graphql.git +git remote add upstream git@github.com:neo4j/graphql.git ``` You can then fetch and merge from the upstream to keep in sync.