Skip to content

Commit

Permalink
Merge changes from master into 2.0.0 (#309)
Browse files Browse the repository at this point in the history
* Missing copyright headers

* Point to prerelease documentation for 2.0.0 branch

* General housekeeping before release

* Update code comment

* Fix TCK tests broken from merge

* fix(jwt): req.cookies might be undefined

this fix prevents the app from crashing id req.cookies is undefined

* Add scalars earlier in schema augmentation for use in types and interfaces without throwing Error (fixes DateTime relationship properties)

* Changes to accomodate merge from master

* fix: use package json for useragent name and version (#271)

* fix: use package json for useragent name and version

* fix: add userAgent support for <=4.2 and >=4.3 drivers

* config: remove codeowners (#277)

* Version update

* fix(login): avoid confusion caused by secondary button (#265)

* fix: losing params while creating Auth Predicate (#281)

* fix: loosing params while creating Auth Predicate

* fix: typos

* fix: typo

* feat: add projection to top level cypher directive (#251)

* feat: add projection to top level queries and mutations using cypher directive

* fix: add missing cypherParams

* Fix for loss of scalar and field level resolvers (#297)

* wrapCustomResolvers removed in favour of schema level resolver auth injection

* Add test cases for this fix

* Mention double escaping for @cypher directive

* Version update

* Allows users to pass in decoded JWT (#303)

* Allows users to pass in decoded JWT - needs more testing

* More tests for decoded JWTs

* Updates to auth documentation

* Fix relationships documentation examples (#296)

* Version update

Co-authored-by: gaspard <gaspard@gmail.com>
Co-authored-by: Oskar Hane <oh@oskarhane.com>
Co-authored-by: Daniel Starns <danielstarns@hotmail.com>
Co-authored-by: Neo Technology Build Agent <continuous-integration+build-agent@neotechnology.com>
Co-authored-by: Arnaud Gissinger <37625778+mathix420@users.noreply.github.com>
  • Loading branch information
6 people authored Jul 12, 2021
1 parent 45ce973 commit 2589937
Show file tree
Hide file tree
Showing 13 changed files with 554 additions and 277 deletions.
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",
"@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

0 comments on commit 2589937

Please sign in to comment.