Authentication and Authorization
The largest breaking change in version 4.0.0 is the removal of the @auth
directive, which requires a migration to the new @authentication
, @authorization
, and @subscriptionsAuthorization
directives.
Instantiation
While the @auth
directive required the installation of an additional plugin package, the functionality for the new directives is now built directly into the library.
To start using it, you should uninstall the previous plugin:
npm uninstall @neo4j/graphql-plugin-auth
Symmetric secret
Given this example of instantiation using a symmetric secret with the plugin:
new Neo4jGraphQL({
typeDefs,
plugins: {
auth: new Neo4jGraphQLAuthJWTPlugin({
secret: "secret",
}),
}
})
You can delete the import of Neo4jGraphQLAuthJWTPlugin
and change the instantiation to:
new Neo4jGraphQL({
typeDefs,
features: {
authorization: {
key: "secret",
}
}
})
JWKS endpoint
When using a JWKS endpoint, an example of how this might be configured currently is:
new Neo4jGraphQL({
typeDefs,
plugins: {
auth: new Neo4jGraphQLAuthJWKSPlugin({
jwksEndpoint: "https://YOUR_DOMAIN/well-known/jwks.json",
}),
}
})
In version 4.0.0, delete the import of Neo4jGraphQLAuthJWKSPlugin
, and change the instantiation to:
new Neo4jGraphQL({
typeDefs,
features: {
authorization: {
key: {
url: "https://YOUR_DOMAIN/well-known/jwks.json",
},
}
}
})
Server
Previously, you could pass in the entire request object and the library would find the Authorization
header:
const server = new ApolloServer({
schema, // schema from Neo4jGraphQL.getSchema()
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({ req }),
});
With the new implementation, the library expects the Authorization
header to be extracted in the format of a bearer token in the token
field of the context:
const server = new ApolloServer({
schema, // schema from Neo4jGraphQL.getSchema()
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => ({
token: req.headers.authorization,
}),
});
This is to acknowledge the fact that there are a variety of servers which don’t have a req
object (such as serverless functions, which use event
).
rolesPath
The rolesPath
argument was used to configure a custom path for the "roles" claim in the JWT structure.
This configuration has now been moved into the type definitions themselves.
Given a previous instantiation:
new Neo4jGraphQL({
typeDefs,
plugins: {
auth: new Neo4jGraphQLAuthJWTPlugin({
secret: "secret",
rolesPath: "some.nested.path",
}),
}
})
This now needs to instead be configured in the type definitions as:
type JWT @jwt {
roles: [String!]! @jwtClaim(path: "some.nested.path")
}
The type name itself can be anything, as long as it is decorated by @jwt
.
Despite the extra setup steps required in 4.0.0, the strongly typed nature of the definition means there is now significantly more powerful filtering options.
Global authentication
Global authentication was previously configured in the auth plugin constructor, for instance:
new Neo4jGraphQL({
typeDefs,
plugins: {
auth: new Neo4jGraphQLAuthJWTPlugin({
secret: "secret",
globalAuthentication: true,
}),
}
})
To remain consistent with the use of directives for configuration, this is now achieved in type definitions by extending the schema:
extend schema @authentication
Rules
allow
Given an allow
rule, which checks the id
field of a User
against the JWT subject before any operation:
type User @auth(rules: [{ allow: { id: "$jwt.sub" } }]) {
id: ID!
}
This is now:
type User @authorization(validate: [{ when: [BEFORE], where: { node: { id: "$jwt.sub" } } }]) {
id: ID!
}
Note that allow
is no longer a discrete rule, but configured by a when
argument, which is an array accepting the values BEFORE
and AFTER
.
bind
Given a bind
rule, which checks the id
field of a User
against the JWT subject after any operation:
type User @auth(rules: [{ bind: { id: "$jwt.sub" } }]) {
id: ID!
}
This is now:
type User @authorization(validate: [{ when: [AFTER], where: { node: { id: "$jwt.sub" } } }]) {
id: ID!
}
Note that bind
is no longer a discrete rule, but configured by a when
argument which is an array accepting values BEFORE
and AFTER
.
isAuthenticated
There isn’t a direct replacement for the |
Given a previous type definition, which required authentication for any operation on the type User
:
type User @auth(rules: [{ isAuthenticated: true }]) {
id: ID!
}
There is not a rule under @authorization
anymore, but the closest is:
type User @authentication {
id: ID!
}
The difference here being that, for example, given the following query:
{
users(where: { id: "1" }) {
id
}
}
-
@auth(rules: [{ isAuthenticated: true }])
only throws an error if thewhere: { id: "1" }
filter results in a match on aUser
. -
@authentication
always throws an error if a user is not authenticated. This happens before the database execution in order to restrict database access to queries generated by authenticated users only.
roles
For these examples, the following is required in the type definitions:
type JWT @jwt {
roles: [String!]!
}
Given the following type definition, which requires a user to have the admin
role to perform any operation on the type User
:
type User @auth(rules: [{ roles: "admin" }]) {
id: ID!
}
This is now:
type User @authorization(validate: [{ where: { jwt: { roles_INCLUDES: "admin" } } }]) {
id: ID!
}
The following changes were made for this migration:
-
If a
validate
rule has been used, it throws an error without the role as per theroles
argument in the@auth
directive. This can alternatively be afilter
rule to just return zero results if a user does not have the required role. -
roles
has becomeroles_INCLUDES
, because the full filtering capabilities of the library can now be used within the@authorization
directive. -
roles
is no longer a top-level rule field, but nested withinwhere
, underjwt
. Any number of JWT claims can now be compared against, if configured within the type decorated with@jwt
.