Setup

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:

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.

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:

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.

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:

interface Neo4jGraphQLAuthPlugin {
    rolesPath?: string;
    isGlobalAuthenticationEnabled?: boolean;

    decode<T>(token: string | any): Promise<T | undefined>;
}

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 Passing in JWTs below. Note that the plugin’s base decode method only supports HS256 and RS256 algorithms.

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:

{
    "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:

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:

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.

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:

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):

neoSchema.getSchema().then((schema) => {
    const server = new ApolloServer({
        schema,
        context: ({ 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:

// 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:

const decodedJWT = decodeJWT(encodedJWT)

neoSchema.getSchema().then((schema) => {
    const server = new ApolloServer({
        schema,
        context: { 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:

const typeDefs = `
    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:

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:

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