Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge changes from master into 2.0.0 #309

Merged
merged 39 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c8e1afe
Missing copyright headers
darrellwarde Jun 15, 2021
900bb4b
Point to prerelease documentation for 2.0.0 branch
darrellwarde Jun 15, 2021
20ff60f
General housekeeping before release
darrellwarde Jun 15, 2021
fb03f39
Update code comment
darrellwarde Jun 15, 2021
ec5883a
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jun 15, 2021
9020391
Merge branch 'master' into 2.0.0
darrellwarde Jun 16, 2021
2568902
Fix TCK tests broken from merge
darrellwarde Jun 16, 2021
5965c75
fix(jwt): req.cookies might be undefined
gasp Jun 16, 2021
b479c85
Add scalars earlier in schema augmentation for use in types and inter…
darrellwarde Jun 18, 2021
dd96ac9
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jun 18, 2021
90b388f
Merge branch 'master' into 2.0.0
darrellwarde Jun 18, 2021
ebc95e8
Changes to accomodate merge from master
darrellwarde Jun 18, 2021
af33c5e
Merge pull request #267 from gasp/fix/jwt-cookie-optional-chaining
oskarhane Jun 22, 2021
6d1853d
fix: use package json for useragent name and version (#271)
danstarns Jun 22, 2021
689c778
config: remove codeowners (#277)
danstarns Jun 22, 2021
283b179
Version update
Jun 22, 2021
1972059
Merge branch 'release/06-22-2021'
Jun 22, 2021
00871c8
fix(login): avoid confusion caused by secondary button (#265)
gasp Jun 28, 2021
502bd00
Merge branch '2.0.0' of github.com:neo4j/graphql into 2.0.0
darrellwarde Jun 29, 2021
2b1f1b3
Merge branch '2.0.0' of github.com:darrellwarde/graphql into 2.0.0
darrellwarde Jun 29, 2021
ba9ee02
fix: losing params while creating Auth Predicate (#281)
mathix420 Jun 30, 2021
6fdaeda
feat: add projection to top level cypher directive (#251)
danstarns Jun 30, 2021
5261188
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 1, 2021
559cd18
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 2, 2021
0107313
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 5, 2021
5e51f67
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 5, 2021
7e531cf
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 5, 2021
662acc3
Fix for loss of scalar and field level resolvers (#297)
darrellwarde Jul 6, 2021
e55bd53
Mention double escaping for @cypher directive
darrellwarde Jul 6, 2021
1d5351d
Merge pull request #299 from darrellwarde/docs/cypher-escaping
darrellwarde Jul 6, 2021
46f64c6
Version update
Jul 6, 2021
01a7c06
Merge branch 'release/1.1.0'
Jul 6, 2021
dea402b
Merge branch 'master' into 2.0.0
darrellwarde Jul 8, 2021
4971e65
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 8, 2021
5e928f6
Allows users to pass in decoded JWT (#303)
darrellwarde Jul 9, 2021
decaf85
Version update
Jul 12, 2021
e0220be
Merge branch 'release/07-12-2021'
Jul 12, 2021
7cbe049
Merge remote-tracking branch 'upstream/2.0.0' into 2.0.0
darrellwarde Jul 12, 2021
6db4f39
Merge branch 'master' into 2.0.0
darrellwarde Jul 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
203 changes: 102 additions & 101 deletions docs/asciidoc/auth/setup.adoc
Original file line number Diff line number Diff line change
@@ -1,67 +1,119 @@
[[auth-setup]]
= Setup

The auth implementation uses JWT tokens. You are expected to pass a JWT into the request. The accepted token type should be Bearer where the header should be authorization;
== Configuration

[source]
----
POST / HTTP/1.1
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyb2xlcyI6WyJ1c2VyX2FkbWluIiwicG9zdF9hZG1pbiIsImdyb3VwX2FkbWluIl19.IY0LWqgHcjEtOsOw60mqKazhuRFKroSXFQkpCtWpgQI
content-type: application/json
----
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:

== Config
- `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"

Auth centric values on the Config object passed to Neo4jGraphQL or OGM.
The simplest construction of a `Neo4jGraphQL` instance would be:

.Auth Config
|===
|Variable | Usage

|`secret`
| Specify JWT secret

|`noVerify`
| Disable the verification of the JWT
[source, javascript]
----
const neoSchema = new Neo4jGraphQL({
typeDefs,
config: {
jwt: {
secret
}
}
});
----

|`rolesPath`
| Specify where on the JWT the roles key is
|===
It is also possible to pass in JWTs which have already been decoded, in which case the `jwt` option is _not necessary_. This will be covered in the section <<auth-setup-passing-in>>.

== Server Construction
=== 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:

*An object key `req` or `request` must be passed into the context before you can use auth.* This object must contain request headers, including the authorization header containing the JWT for each request.
[source, json]
----
{
"https://auth0.mysite.com/claims": {
"https://auth0.mysite.com/claims/roles": ["admin"]
}
}
----

Here is an example using Apollo Server:
In order to make use of this, you must pass it in as a "dot path" into the `rolesPath` option:

[source, javascript]
----
const neoSchema = new Neo4jGraphQL({
typeDefs,
config: {
jwt: {
secret
secret,
rolesPath: "https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles"
}
}
});
----

[[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]
----
const server = new ApolloServer({
schema: neoSchema.schema,
context: ({ req }) => ({ req }),
});
----

We expect there to be a `headers` key within either a `req` or `request` object in the context. If for example, you are using `apollo-server-lambda` to host a GraphQL API as a lambda function, the context function will have an `event` object which will need to be renamed to `req`. For example:
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]
----
// 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]
----
const decodedJWT = decodeJWT(encodedJWT)

const server = new ApolloServer({
schema: neoSchema.schema,
context: ({ event }) => ({ req: event }),
context: { jwt: decodedJWT.payload },
});
----

== `rules`
== `@auth` directive

=== `rules`

You can have many rules for many operations. We fall through each rule, on the corresponding operation, until we find a match. On no match found, an error is thrown. You can think of rules as a big OR.

Expand All @@ -74,9 +126,9 @@ You can have many rules for many operations. We fall through each rule, on the c
])
----

== `operations`
=== `operations`

Operations is an array, you can re-use the same rule for many operations.
Operations is an array which allows you to re-use the same rule for many operations.

[source, graphql]
----
Expand All @@ -86,22 +138,18 @@ Operations is an array, you can re-use the same rule for many operations.
])
----

> The absence of an `operations` argument will imply all operations
NOTE: Note that the absence of an `operations` argument will imply _all_ operations.

Many different operations can be called in one query take the below mutation;
Many different operations can be called at once, for example in the following Mutation:

[source, graphql]
----
mutation {
createPosts(
input: [
{
content: "I like GraphQL"
creator: {
connect: {
where: { node: { id: "user-01" } }
}
}
content: "I like GraphQL",
creator: { connect: { where: { id: "user-01" } } }
}
]
) {
Expand All @@ -112,44 +160,16 @@ mutation {
}
----

In the above example; First we do a `create` operation then we do a `connect` operation.
In the above example, we perform a `CREATE` followed by a `CONNECT`, so our auth rule must allow our user to perform both of these operations.

The full list of operations are;
The full list of operations are:

1. read - `MATCH`
2. create - `CREATE`
3. update - `SET`
4. delete - `DELETE`
5. connect - `MATCH` & `MERGE`
6. disconnect - `MATCH` & `DELETE`


== Auth Roles Object Paths
If you are using 3rd party Auth solutions such as Auth0 you may find your roles property being nested inside an object;

[source, json]
----
{
"https://auth0.mysite.com/claims": {
"https://auth0.mysite.com/claims/roles": ["admin"]
}
}
----

Specify the key at construction:

[source, javascript]
----
const neoSchema = new Neo4jGraphQL({
typeDefs,
config: {
jwt: {
secret,
rolesPath: "https://auth0.mysite.com/claims\\.https://auth0.mysite.com/claims/roles"
}
}
});
----
- read - `MATCH`
- create - `CREATE`
- update - `SET`
- delete - `DELETE`
- connect - `MATCH` & `MERGE`
- disconnect - `MATCH` & `DELETE`

== Auth Value Plucking

Expand All @@ -158,70 +178,51 @@ const neoSchema = new Neo4jGraphQL({

== Auth Custom Resolvers

You cant put the auth directive on a custom resolver. We do make life easier by injecting the auth param into it. It will be available under the `context.auth` property;
You can't use the `@auth` directive on a custom resolver, however, we do make life easier by injecting the auth parameter into it. It will be available under the `context.auth` property. For example, the following custom resolver returns the `sub` field from the JWT:

[source, javascript]
----
const { Neo4jGraphQL } = require("@neo4j/graphql")
const neo4j = require("neo4j-driver");
const { ApolloServer } = require("apollo-server")

const typeDefs = `
type User {
id: ID!
email: String!
password: String!
}
type Query {
myId: ID!
}
`;

const driver = neo4j.driver(
"bolt://localhost:7687",
neo4j.auth.basic("admin", "password")
);

const resolvers = {
Query: {
myId(root, args, context) {
return context.auth.jwt.sub
}
}
};

const neoSchema = new Neo4jGraphQL({ typeDefs, resolvers, config: { jwt } });

const server = new ApolloServer({
schema: neo4jGraphQL.schema,
context: ({ req }) => ({ req, driver }),
});

server.listen(4000).then(() => console.log("online"));
----

== Auth on `@cypher`

You can put the `@auth` directive on a field with the `@cypher` directive. Functionality like allow and bind will not work but you can still utilize `isAuthenticated` and `roles`.
You can put the `@auth` directive on a field with 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]
----
type User @exclude {
id: ID
name: String
}

type Query {
users: [User] @cypher(statement: "MATCH (a:User) RETURN a") @auth(rules: [{ isAuthenticated: true }])
me: User @cypher(statement: "MATCH (u:User { id: $auth.jwt.sub }) RETURN u") @auth(rules: [{ isAuthenticated: true }])
}
----

Notice you don't need to specify operations for `@auth` directives on `@cypher` fields.
In the following example, the current user must have role "admin" in order to query the `history` field on the type `User`:

[source, graphql]
----
type History @exclude {
website: String!
}

type User {
id: ID
name: String
Expand Down
4 changes: 2 additions & 2 deletions docs/asciidoc/type-definitions/relationships.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ We then need to create the actor in our example, and connect them to the new Mov
[source, graphql]
----
mutation CreateActor {
createActors(input: [
createPeople(input: [
{
name: "Tom Hanks"
born: 1956
movies: {
actedInMovies: {
connect: {
where: {
node: { title: "Forrest Gump" }
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@types/faker": "5.1.7",
"@types/is-uuid": "1.0.0",
"@types/jest": "26.0.8",
"@types/jsonwebtoken": "8.5.0",
"@types/jsonwebtoken": "^8.5.4",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only one that's variable 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're a bit all over the place with this, our actual dependencies have it, but our devDependencies are locked - these could probably all also be prefixed with ^!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not change anything seeming it works just fine, I fear adding a ^ could introduce unforeseen issues.

"@types/node": "14.0.27",
"@types/pluralize": "0.0.29",
"@types/randomstring": "1.1.6",
Expand Down
4 changes: 3 additions & 1 deletion packages/graphql/src/classes/Neo4jGraphQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ class Neo4jGraphQL {

context.resolveTree = getNeo4jResolveTree(resolveInfo);

context.jwt = getJWT(context);
if (!context.jwt) {
context.jwt = getJWT(context);
}

context.auth = createAuthParam({ context });
});
Expand Down
4 changes: 4 additions & 0 deletions packages/graphql/src/translate/create-auth-and-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ function createAuthPredicate({
}
const { jwt } = context;

if (!jwt) {
throw new Error("Can't generate auth predicate - no JWT in context");
}

const result = Object.entries(rule[kind] as any).reduce(
(res: Res, [key, value]) => {
if (key === "AND" || key === "OR") {
Expand Down
6 changes: 1 addition & 5 deletions packages/graphql/src/translate/create-auth-param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@ function createAuthParam({ context }: { context: Context }) {

const jwtConfig = context.neoSchema.config?.jwt;

if (!jwtConfig) {
return param;
}

if (jwtConfig.rolesPath) {
if (jwtConfig?.rolesPath) {
param.roles = dotProp.get(jwt, jwtConfig.rolesPath);
} else if (jwt.roles) {
param.roles = jwt.roles;
Expand Down
Loading