Skip to content

Commit 35a8bea

Browse files
Claudio Gonzalezcursoragent
andcommitted
feat(auth): add Envelop plugin and bump version to 2.4.1
Add createAuthPlugin() as a native Envelop/Yoga alternative to graphql-middleware's applyMiddleware, which causes duplicate-type errors with Simfinity schemas. Mark createAuthMiddleware and createFieldMiddleware as deprecated. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d449fd6 commit 35a8bea

5 files changed

Lines changed: 601 additions & 72 deletions

File tree

README.md

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
2020
- [Adding Middlewares](#adding-middlewares)
2121
- [Middleware Parameters](#middleware-parameters)
2222
- [Common Use Cases](#common-use-cases)
23-
- [Authorization Middleware](#-authorization-middleware)
23+
- [Authorization](#-authorization)
2424
- [Quick Start](#quick-start-1)
2525
- [Permission Schema](#permission-schema)
2626
- [Rule Helpers](#rule-helpers)
2727
- [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
28-
- [Integration with graphql-middleware](#integration-with-graphql-middleware)
28+
- [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
29+
- [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
2930
- [Relationships](#-relationships)
3031
- [Defining Relationships](#defining-relationships)
3132
- [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
@@ -75,7 +76,7 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
7576
- **Lifecycle Hooks**: Controller methods for granular control over operations
7677
- **Custom Validation**: Field-level and type-level custom validations
7778
- **Relationship Management**: Support for embedded and referenced relationships
78-
- **Authorization Middleware**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, and declarative policy expressions
79+
- **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
7980

8081
## 📦 Installation
8182

@@ -629,17 +630,17 @@ simfinity.use((params, next) => {
629630
5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
630631
6. **Use context wisely**: Store request-specific data in the GraphQL context object
631632
632-
## 🔐 Authorization Middleware
633+
## 🔐 Authorization
633634
634-
Simfinity.js provides a production-grade centralized GraphQL authorization middleware supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies.
635+
Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.
635636
636637
### Quick Start
637638
638639
```javascript
639640
const { auth } = require('@simtlix/simfinity-js');
640-
const { applyMiddleware } = require('graphql-middleware');
641+
const { createYoga } = require('graphql-yoga');
641642
642-
const { createAuthMiddleware, requireAuth, requireRole } = auth;
643+
const { createAuthPlugin, requireAuth, requireRole } = auth;
643644
644645
// Define your permission schema
645646
const permissions = {
@@ -665,9 +666,9 @@ const permissions = {
665666
},
666667
};
667668
668-
// Create and apply the middleware
669-
const authMiddleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
670-
const schemaWithAuth = applyMiddleware(schema, authMiddleware);
669+
// Create the Envelop auth plugin and pass it to your server
670+
const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
671+
const yoga = createYoga({ schema, plugins: [authPlugin] });
671672
```
672673
673674
### Permission Schema
@@ -869,25 +870,24 @@ Use `{ ref: 'path' }` to reference values:
869870
- Unknown operators fail closed (deny)
870871
- No `eval()` or `Function()` - pure object traversal
871872
872-
### Integration with graphql-middleware
873+
### Integration with GraphQL Yoga / Envelop
873874
874-
The auth middleware integrates with the `graphql-middleware` package:
875+
The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.
875876
876877
```javascript
877-
const express = require('express');
878-
const { graphqlHTTP } = require('express-graphql');
879-
const { applyMiddleware } = require('graphql-middleware');
878+
const { createYoga } = require('graphql-yoga');
879+
const { createServer } = require('http');
880880
const simfinity = require('@simtlix/simfinity-js');
881881
882882
const { auth } = simfinity;
883-
const { createAuthMiddleware, requireAuth, requireRole, requirePermission } = auth;
883+
const { createAuthPlugin, requireAuth, requireRole, requirePermission } = auth;
884884
885885
// Define your types and connect them
886886
simfinity.connect(null, UserType, 'user', 'users');
887887
simfinity.connect(null, PostType, 'post', 'posts');
888888
889889
// Create base schema
890-
const baseSchema = simfinity.createSchema();
890+
const schema = simfinity.createSchema();
891891
892892
// Define permissions
893893
const permissions = {
@@ -921,36 +921,51 @@ const permissions = {
921921
},
922922
};
923923
924-
// Create auth middleware
925-
const authMiddleware = createAuthMiddleware(permissions, {
926-
defaultPolicy: 'DENY', // Deny access when no rule matches
927-
debug: false, // Enable for debugging
924+
// Create auth plugin
925+
const authPlugin = createAuthPlugin(permissions, {
926+
defaultPolicy: 'DENY',
927+
debug: false,
928928
});
929929
930-
// Apply middleware to schema
931-
const schema = applyMiddleware(baseSchema, authMiddleware);
932-
933-
// Setup Express with context
934-
const app = express();
935-
936-
app.use('/graphql', graphqlHTTP((req) => ({
930+
// Setup Yoga with the auth plugin
931+
const yoga = createYoga({
937932
schema,
938-
graphiql: true,
939-
context: {
940-
user: req.user, // Set by your authentication middleware
941-
},
942-
formatError: simfinity.buildErrorFormatter((err) => {
943-
console.error(err);
933+
plugins: [authPlugin],
934+
context: (req) => ({
935+
user: req.user, // Set by your authentication layer
944936
}),
945-
})));
937+
});
946938
947-
app.listen(4000);
939+
const server = createServer(yoga);
940+
server.listen(4000);
941+
```
942+
943+
### Legacy: Integration with graphql-middleware
944+
945+
> **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
946+
> which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
947+
> Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.
948+
949+
```javascript
950+
const { applyMiddleware } = require('graphql-middleware');
951+
const simfinity = require('@simtlix/simfinity-js');
952+
953+
const { auth } = simfinity;
954+
const { createAuthMiddleware, requireAuth, requireRole } = auth;
955+
956+
const baseSchema = simfinity.createSchema();
957+
958+
const authMiddleware = createAuthMiddleware(permissions, {
959+
defaultPolicy: 'DENY',
960+
});
961+
962+
const schema = applyMiddleware(baseSchema, authMiddleware);
948963
```
949964
950-
### Middleware Options
965+
### Plugin / Middleware Options
951966
952967
```javascript
953-
const middleware = createAuthMiddleware(permissions, {
968+
const plugin = createAuthPlugin(permissions, {
954969
defaultPolicy: 'DENY', // 'ALLOW' or 'DENY' (default: 'DENY')
955970
debug: false, // Enable debug logging
956971
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@simtlix/simfinity-js",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "",
55
"main": "src/index.js",
66
"type": "module",

src/auth/index.js

Lines changed: 110 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Simfinity GraphQL Authorization Middleware
2+
* Simfinity GraphQL Authorization
33
*
44
* Production-grade centralized GraphQL authorization supporting:
55
* - RBAC / ABAC
@@ -10,9 +10,9 @@
1010
*
1111
* @example
1212
* import { auth } from '@simtlix/simfinity-js';
13-
* import { applyMiddleware } from 'graphql-middleware';
13+
* import { createYoga } from 'graphql-yoga';
1414
*
15-
* const { createAuthMiddleware, requireAuth, requireRole } = auth;
15+
* const { createAuthPlugin, requireAuth, requireRole } = auth;
1616
*
1717
* const permissions = {
1818
* Query: {
@@ -28,10 +28,11 @@
2828
* }
2929
* };
3030
*
31-
* const authMiddleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
32-
* const schemaWithAuth = applyMiddleware(schema, authMiddleware);
31+
* const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
32+
* const yoga = createYoga({ schema, plugins: [authPlugin] });
3333
*/
3434

35+
import { GraphQLObjectType, defaultFieldResolver } from 'graphql';
3536
import { UnauthenticatedError, ForbiddenError, createAuthError } from './errors.js';
3637
import { isPolicyExpression, createRuleFromExpression, evaluateExpression } from './expressions.js';
3738
import {
@@ -169,33 +170,14 @@ const executeRule = async (rule, parent, args, ctx, info) => {
169170
};
170171

171172
/**
172-
* Creates a graphql-middleware compatible authorization middleware
173+
* Creates a graphql-middleware compatible authorization middleware.
174+
*
175+
* @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
176+
* can cause duplicate-type errors when the schema contains custom introspection extensions.
173177
*
174178
* @param {PermissionSchema} permissions - The permission schema object
175179
* @param {AuthMiddlewareOptions} [options={}] - Middleware options
176180
* @returns {Function} A graphql-middleware compatible middleware function
177-
*
178-
* @example
179-
* const permissions = {
180-
* Query: {
181-
* users: requireAuth(),
182-
* adminDashboard: requireRole('ADMIN')
183-
* },
184-
* User: {
185-
* '*': requireAuth(),
186-
* email: requireRole('ADMIN')
187-
* },
188-
* Post: {
189-
* content: {
190-
* anyOf: [
191-
* { eq: [{ ref: 'parent.published' }, true] },
192-
* { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] }
193-
* ]
194-
* }
195-
* }
196-
* };
197-
*
198-
* const middleware = createAuthMiddleware(permissions, { defaultPolicy: 'DENY' });
199181
*/
200182
export const createAuthMiddleware = (permissions, options = {}) => {
201183
const {
@@ -250,8 +232,11 @@ export const createAuthMiddleware = (permissions, options = {}) => {
250232
};
251233

252234
/**
253-
* Creates a field-level middleware object from a permission schema
254-
* This can be used with graphql-middleware's applyMiddleware
235+
* Creates a field-level middleware object from a permission schema.
236+
* This can be used with graphql-middleware's applyMiddleware.
237+
*
238+
* @deprecated Use {@link createAuthPlugin} instead. `applyMiddleware` from graphql-middleware
239+
* can cause duplicate-type errors when the schema contains custom introspection extensions.
255240
*
256241
* @param {PermissionSchema} permissions - The permission schema
257242
* @param {AuthMiddlewareOptions} [options={}] - Middleware options
@@ -277,9 +262,103 @@ export const createFieldMiddleware = (permissions, options = {}) => {
277262
return fieldMiddleware;
278263
};
279264

265+
/**
266+
* Creates an Envelop-compatible authorization plugin that wraps schema resolvers in-place.
267+
*
268+
* Unlike {@link createAuthMiddleware} (which requires graphql-middleware's `applyMiddleware`
269+
* and rebuilds the schema), this plugin mutates resolvers directly on the existing schema,
270+
* avoiding schema reconstruction and the duplicate-type errors it can cause.
271+
*
272+
* @param {PermissionSchema} permissions - The permission schema object
273+
* @param {AuthMiddlewareOptions} [options={}] - Plugin options
274+
* @returns {Object} An Envelop plugin with an `onSchemaChange` hook
275+
*
276+
* @example
277+
* import { auth } from '@simtlix/simfinity-js';
278+
* import { createYoga } from 'graphql-yoga';
279+
*
280+
* const permissions = {
281+
* Query: {
282+
* users: requireAuth(),
283+
* adminDashboard: requireRole('ADMIN')
284+
* },
285+
* User: {
286+
* '*': requireAuth(),
287+
* email: requireRole('ADMIN')
288+
* }
289+
* };
290+
*
291+
* const authPlugin = auth.createAuthPlugin(permissions, { defaultPolicy: 'DENY' });
292+
* const yoga = createYoga({ schema, plugins: [authPlugin] });
293+
*/
294+
export const createAuthPlugin = (permissions, options = {}) => {
295+
const {
296+
defaultPolicy = 'DENY',
297+
debug = false,
298+
} = options;
299+
300+
const log = debug ? console.log.bind(console, '[auth]') : () => {};
301+
const processedSchemas = new WeakSet();
302+
303+
const wrapSchemaResolvers = (schema) => {
304+
if (processedSchemas.has(schema)) return;
305+
306+
const typeMap = schema.getTypeMap();
307+
308+
for (const [typeName, type] of Object.entries(typeMap)) {
309+
if (!(type instanceof GraphQLObjectType) || typeName.startsWith('__')) continue;
310+
311+
const fields = type.getFields();
312+
313+
for (const [fieldName, field] of Object.entries(fields)) {
314+
const rules = getFieldRules(permissions, typeName, fieldName);
315+
const originalResolve = field.resolve || defaultFieldResolver;
316+
317+
field.resolve = async (parent, args, ctx, info) => {
318+
log(`Checking ${typeName}.${fieldName}`);
319+
320+
if (rules === null || rules.length === 0) {
321+
log(`No rules for ${typeName}.${fieldName}, applying default policy: ${defaultPolicy}`);
322+
323+
if (defaultPolicy === 'DENY') {
324+
throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
325+
}
326+
327+
return originalResolve(parent, args, ctx, info);
328+
}
329+
330+
for (const rule of rules) {
331+
log(`Executing rule for ${typeName}.${fieldName}`);
332+
333+
const allowed = await executeRule(rule, parent, args, ctx, info);
334+
335+
if (!allowed) {
336+
log(`Rule denied access to ${typeName}.${fieldName}`);
337+
throw new ForbiddenError(`Access denied to ${typeName}.${fieldName}`);
338+
}
339+
}
340+
341+
log(`Access granted to ${typeName}.${fieldName}`);
342+
343+
return originalResolve(parent, args, ctx, info);
344+
};
345+
}
346+
}
347+
348+
processedSchemas.add(schema);
349+
};
350+
351+
return {
352+
onSchemaChange({ schema }) {
353+
wrapSchemaResolvers(schema);
354+
},
355+
};
356+
};
357+
280358
// Default export with all auth utilities
281359
const auth = {
282-
// Main factory
360+
// Main factories
361+
createAuthPlugin,
283362
createAuthMiddleware,
284363
createFieldMiddleware,
285364

src/plugins.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { createAuthPlugin } from './auth/index.js';
2+
3+
export { createAuthPlugin } from './auth/index.js';
4+
15
/**
26
* Apollo Server plugin to add count to GraphQL response extensions
37
* @returns {Object} Apollo Server plugin
@@ -40,11 +44,10 @@ export const envelopCountPlugin = () => {
4044
};
4145
};
4246

43-
// Export all plugins as an object for convenience
4447
const plugins = {
48+
createAuthPlugin,
4549
apolloCountPlugin,
4650
envelopCountPlugin,
4751
};
4852

4953
export default plugins;
50-

0 commit comments

Comments
 (0)