Skip to content

Allows users to pass in decoded JWT #303

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

Merged
merged 4 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.
198 changes: 108 additions & 90 deletions docs/asciidoc/auth/setup.adoc
Original file line number Diff line number Diff line change
@@ -1,54 +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
[source, javascript]
----
const neoSchema = new Neo4jGraphQL({
typeDefs,
config: {
jwt: {
secret
}
}
});
----

|`secret`
| Specify JWT secret
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>>.

|`noVerify`
| Disable the verification of the JW
=== 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:

|`rolesPath`
| Specify where on the JWT the roles key is
|===
[source, json]
----
{
"https://auth0.mysite.com/claims": {
"https://auth0.mysite.com/claims/roles": ["admin"]
}
}
----

== Server Construction
Request object needs to be injected into the context before you can use auth. 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 }),
});
----

== `rules`
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: { jwt: decodedJWT.payload },
});
----

== `@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 @@ -61,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 @@ -73,9 +138,9 @@ 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]
----
Expand All @@ -95,44 +160,16 @@ mutation {
}
----

In the above example; First we do a `create` operation then we do a `connect` operation.

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"]
}
}
----
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.

Specify the key at construction:
The full list of operations are:

[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 @@ -141,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 @@ -87,11 +87,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: {
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 @@ -131,7 +131,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");
Copy link
Contributor

Choose a reason for hiding this comment

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

This condition is making the whole graphql schema unusable for a non-authed user if jwt: {} is not set in the context.
It may be good to update docs accordingly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hey @mathix420. This doesn't sound like a feature! Could you provide something reproducible?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hello @danstarns! I will try to give you that today, but i'm quite busy right now so it might be for this weekend.

Copy link
Contributor

@mathix420 mathix420 Jul 16, 2021

Choose a reason for hiding this comment

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

Eventually work can wait 10 minutes 😅

const { Neo4jGraphQL } = require("@neo4j/graphql");
const { ApolloServer } = require("apollo-server");
const neo4j = require("neo4j-driver");
require('dotenv').config();

const AURA_ENDPOINT = process.env.NEO4J_URL;
const PASSWORD = process.env.NEO4J_PASS;
const USERNAME = 'neo4j';

const typeDefs = `
    type Video {
        id: ID! @id
        title: String!
        publisher: String!
    }

    extend type Video @auth(rules: [
        { where: { publisher: "$jwt.sub" } },
    ]),
`;

const driver = neo4j.driver(AURA_ENDPOINT, neo4j.auth.basic(USERNAME, PASSWORD),{
      disableLosslessIntegers: true,
});

const neoSchema = new Neo4jGraphQL({ typeDefs, driver });

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

server.listen(4000).then(() => console.log("Online"));
query listVideos {
    videos {
        id
        title
    }
}
{
  "errors": [
    {
      "message": "Can't generate auth predicate - no JWT in context",
      ...
    }
  ],
  "data": null
}

I think the issue is comming from the where rule.

Quick workaround

{
  // ...
  context: ({ req }) => ({ req, jwt: {} }),
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@mathix420 Thank you 🙏 we will look into this.

Copy link
Contributor

@mathix420 mathix420 Jul 16, 2021

Choose a reason for hiding this comment

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

EDIT: The "Quick workaround" is actually not working at all 😛
It just avoid to fall into the !jwt condition, but never checks the jwt, and do not load jwt from cookies/headers.

(I have opened a PR on that issue #330)

}

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
10 changes: 9 additions & 1 deletion packages/graphql/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { InputValueDefinitionNode, DirectiveNode } from "graphql";
import { ResolveTree } from "graphql-parse-resolve-info";
import { JwtPayload } from "jsonwebtoken";
import { Driver } from "neo4j-driver";
import { Neo4jGraphQL } from "./classes";

Expand All @@ -27,12 +28,19 @@ export type DriverConfig = {
bookmarks?: string | string[];
};

interface AuthContext {
isAuthenticated: boolean;
roles: [string];
jwt: JwtPayload;
}

export interface Context {
driver: Driver;
driverConfig?: DriverConfig;
resolveTree: ResolveTree;
neoSchema: Neo4jGraphQL;
jwt?: any;
jwt?: JwtPayload;
auth?: AuthContext;
[k: string]: any;
}

Expand Down
Loading