Ok, this worked, well not exactly as i'm now facing a new problem
so as I expected, graphql-shield acts as a middleware between the graphql server (apollo, yoga, mercurius or whatsoever) and neo4j's graphql layer.
So we have to auth with middleware instead of neo4j's auth plugin
but let's start with a super video tutorial from Jamie Barton that I invite anyone interested to watch first
So let's assume that we use neo4j's JWT auth plugin
let's create the shield this way :
// shield.js
import { shield, rule, not, allow } from "graphql-shield"
// IMPORTANT NOTICE : the basic example from their doc forgets to import the
// allow, deny, and, or, not keywords. rules are async, hence return promises. Obviously
// logical operators don't work on promises, hence I guess the role of those operators
import { GraphQLError } from "graphql"
import jwt from "jsonwebtoken"
const isAuthenticated = rule()(async function (parent, args, context, info)
{
console.log ("isAuthenticated / auth header : " + context.req.headers.authorization)
if (!context.req.headers.authorization)
return new GraphQLError ("Unauthenticated (no auth header)", { extensions: { code: 'UNAUTHENTICATED' } })
const token = context.req.headers.authorization.split(' ')[1]
console.log ("isAuthenticated / token : " + token)
if (!token)
return new GraphQLError ("Unauthenticated (no token)", { extensions: { code: 'UNAUTHENTICATED' } })
try
{
const decode = jwt.verify(token, process.env.JWT_SESSION_KEY)
// Here is our problem, obviously neo4j sets context.auth even if no auth plugin has been set
context.auth =
{
isAuthenticated: true,
roles: [],
jwt: decode,
}
console.log ("isAuthenticated / auth.jwt : " + JSON.stringify (context.auth, null, 4))
return true
}
catch (err)
{
console.log (error)
return new GraphQLError ("Unauthenticated", { extensions: { code: 'INVALID_TOKEN' } })
}
return new GraphQLError ("we should not get here")
})
const isUnauthenticated = rule()(async function (parent, args, context, info)
{
console.log ("isUnauthenticated context keys : " + Object.keys(context.req.headers))
if (!context.req.headers.authorization)
return true
return new GraphQLError ("already Authenticated", { extensions: { code: 'ALREADY_AUTHENTICATED' } })
})
const permissions = shield({
Query: { "*": isAuthenticated, },
Mutation:
{
"*": isAuthenticated,
login: isUnauthenticated,
signup: isUnauthenticated,
},
Subscription: { "*": isAuthenticated, },
})
export default permissions
The important thing to note is that graphql-shield by default catches ALL errors thrown to throw their default one instead. So ALWAYS return an error instead of throwing it.
see documentation
So to debug the code you basically have to put a console.log between each line to see where the print stops to locate the error.
So I created an isUnauthenticated (I won't allow a user to log in if he sends me a token) one because I either return true, or an error, so negating the true would return the default error.
Next is the application of the middleware to the shema.
const neo4jgraphql = new Neo4jGraphQL (
{
typeDefs,
resolvers,
driver: neo4jdriver,
plugins:
{
// TODO : replace default subscription plugin by custom redis plugin
subscriptions: new Neo4jGraphQLSubscriptionsSingleInstancePlugin(),
// Don't remove this, neo4j has to perform authentication again to get context.auth properly set ... again
auth: new Neo4jGraphQLAuthJWTPlugin(
{
secret: process.env.JWT_SESSION_KEY,
globalAuthentication: false,
}),
},
})
const neo4jschema = await neo4jgraphql.getSchema()
const neo4jschemaWithShield = applyMiddleware (neo4jschema, permissions)
and finally use the shielded shema instead of the raw schema (with Fastify and Mercurius)
app.register(mercurius,
{
schema: neo4jschemaWithShield,
subscription: { pubsub },
graphiql: environment==='development',
context: (req, res) => { return { req, res, pubsub, serverUrl, dataModel} }
})
In apollo without an express middleware this would be
const server = new ApolloServer(
{
schema: neo4jschemaWithShield
});
const { url } = await startStandaloneServer(server, { listen: { port: 4000 },
});
For authorizations, this is strongly gonna be relationship dependant in my case which is the reason why I just content myself with allowing login and signup to unauthenticated and allowing the remainder to authenticated.
Thanks Liam for the suggestion.