Mercurius Auth Plugin
Mercurius Auth is a Fastify plugin designed to add configurable authentication and authorization support to GraphQL APIs built with Mercurius. It is currently at stable version 6.0.0, with major updates often aligning with new Fastify or Mercurius versions. The plugin allows defining auth directives directly within the GraphQL schema to apply custom policies against protected fields, supporting both normal and gateway modes. Alternatively, it can operate in an 'External Policy' mode, offering programmatic control over authorization. Key differentiators include its tight integration with the Fastify and Mercurius ecosystems, its ability to build an auth context, and its GraphQL spec compliance, including features like schema filtering and replacement. Development appears active, with regular updates and dependency bumps.
Common errors
-
Error: mercuriusAuth must be used as a Fastify plugin, i.e. `fastify.register(mercuriusAuth)`
cause Attempting to initialize `mercurius-auth` directly without using `fastify.register()` or passing incorrect options to `register`.fixAlways register `mercurius-auth` using `app.register(mercuriusAuth, options)` where `app` is your Fastify instance. -
TypeError: Cannot read properties of undefined (reading 'request') in authContext
cause This error often occurs when `context.reply.request` is `undefined` within `authContext`, usually due to an incompatible Fastify version or a misconfigured Mercurius instance not properly passing the Fastify context.fixEnsure your Fastify and Mercurius versions are compatible with `mercurius-auth`. Specifically, `mercurius-auth` v3+ requires Fastify v4+. Also, check your Mercurius registration for proper context propagation. -
Error: Unknown directive "@auth".
cause The `@auth` directive (or your custom directive name) has been used in the schema but was not properly defined in the `mercurius-auth` options or included in the `authDirective` option.fixMake sure your GraphQL schema includes the directive definition (e.g., `directive @auth(...)`) and that the `authDirective` option in `app.register(mercuriusAuth, { authDirective: 'auth' })` matches your directive name.
Warnings
- breaking Version 6.0.0 of mercurius-auth introduces compatibility changes to prepare for Fastify v5. While it might still work with Fastify v4, it is highly recommended to upgrade Fastify to its latest major version alongside mercurius-auth v6.0.0 to ensure full compatibility and avoid potential issues. This aligns with Fastify's plugin API evolution.
- breaking Version 3.0.0 of mercurius-auth upgraded its core dependency to Fastify v4. This is a significant breaking change as Fastify v4 introduced several API changes, particularly around plugin registration and decorators. Directly incompatible Fastify v3 usage will result in runtime errors.
- breaking Mercurius Auth v2.0.2 removed the `@graphql-tools/wrap` dependency and replaced its functionality internally. While this primarily impacted internal implementation, it might have subtle effects if your application relied on specific behaviors or types exposed by that dependency through earlier mercurius-auth versions.
- gotcha When using `mercurius-auth` in ESM modules, ensure you use `import mercuriusAuth from 'mercurius-auth'` instead of `require`. While Fastify and Mercurius generally handle both, mixing module systems can lead to unexpected errors or require specific build configurations.
Install
-
npm install mercurius-auth -
yarn add mercurius-auth -
pnpm add mercurius-auth
Imports
- mercuriusAuth
const mercuriusAuth = require('mercurius-auth')import mercuriusAuth from 'mercurius-auth'
- Fastify
const Fastify = require('fastify')import Fastify from 'fastify'
- mercurius
const mercurius = require('mercurius')import mercurius from 'mercurius'
Quickstart
import Fastify from 'fastify';
import mercurius from 'mercurius';
import mercuriusAuth from 'mercurius-auth';
const app = Fastify();
const schema = `
directive @auth(
requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
UNKNOWN
}
type Query {
add(x: Int, y: Int): Int @auth(requires: USER)
adminData: String @auth(requires: ADMIN)
}
`;
const resolvers = {
Query: {
add: async (_, { x, y }) => x + y,
adminData: async () => 'Secret admin information'
}
};
app.register(mercurius, {
schema,
resolvers
});
app.register(mercuriusAuth, {
authContext (context) {
// Simulate loading user identity from request headers
const identity = context.reply.request.headers['x-user'] || 'UNKNOWN';
return {
identity: identity.toUpperCase() // Ensure consistency
};
},
async applyPolicy (authDirectiveAST, parent, args, context, info) {
const requiredRole = authDirectiveAST.requires;
const userRole = context.auth.identity; // Assuming identity is the role for simplicity
if (requiredRole === 'ADMIN' && userRole !== 'ADMIN') {
return false;
}
if (requiredRole === 'USER' && (userRole !== 'ADMIN' && userRole !== 'USER')) {
return false;
}
return true; // Policy passes
},
authDirective: 'auth'
});
app.listen({ port: 3000 }, (err) => {
if (err) {
app.log.error(err);
process.exit(1);
}
app.log.info(`Server listening on port 3000`);
});
// Example usage (e.g., with curl):
// curl -H "x-user: user" http://localhost:3000/graphql -X POST -H "Content-Type: application/json" -d '{"query":"query { add(x: 5, y: 3) }"}'
// curl -H "x-user: admin" http://localhost:3000/graphql -X POST -H "Content-Type: application/json" -d '{"query":"query { adminData }"}'
// curl -H "x-user: user" http://localhost:3000/graphql -X POST -H "Content-Type: application/json" -d '{"query":"query { adminData }"}' // Should fail