@stonecrop/casl-middleware 0.12.7 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/index.js +3 -0
  2. package/dist/middleware/ability.js +139 -0
  3. package/dist/middleware/graphql.js +120 -0
  4. package/dist/middleware/introspection.js +169 -0
  5. package/dist/middleware/jwt.js +291 -0
  6. package/dist/middleware/postgraphile.js +80 -0
  7. package/dist/middleware/yoga.js +32 -0
  8. package/dist/src/tsdoc-metadata.json +11 -0
  9. package/dist/tests/ability.test.d.ts +2 -0
  10. package/dist/tests/ability.test.d.ts.map +1 -0
  11. package/dist/tests/ability.test.js +402 -0
  12. package/dist/tests/helpers/test-utils.d.ts +46 -0
  13. package/dist/tests/helpers/test-utils.d.ts.map +1 -0
  14. package/dist/tests/helpers/test-utils.js +92 -0
  15. package/dist/tests/introspection.test.d.ts +2 -0
  16. package/dist/tests/introspection.test.d.ts.map +1 -0
  17. package/dist/tests/introspection.test.js +368 -0
  18. package/dist/tests/jwt.test.d.ts +2 -0
  19. package/dist/tests/jwt.test.d.ts.map +1 -0
  20. package/dist/tests/jwt.test.js +700 -0
  21. package/dist/tests/middleware.test.d.ts +2 -0
  22. package/dist/tests/middleware.test.d.ts.map +1 -0
  23. package/dist/tests/middleware.test.js +184 -0
  24. package/dist/tests/postgraphile-plugin.test.d.ts +2 -0
  25. package/dist/tests/postgraphile-plugin.test.d.ts.map +1 -0
  26. package/dist/tests/postgraphile-plugin.test.js +56 -0
  27. package/dist/tests/setup.d.ts +2 -0
  28. package/dist/tests/setup.d.ts.map +1 -0
  29. package/dist/tests/setup.js +11 -0
  30. package/dist/tests/types.test.d.ts +2 -0
  31. package/dist/tests/types.test.d.ts.map +1 -0
  32. package/dist/tests/types.test.js +143 -0
  33. package/dist/tests/user-roles.test.d.ts +2 -0
  34. package/dist/tests/user-roles.test.d.ts.map +1 -0
  35. package/dist/tests/user-roles.test.js +157 -0
  36. package/dist/tests/yoga-plugin.test.d.ts +2 -0
  37. package/dist/tests/yoga-plugin.test.d.ts.map +1 -0
  38. package/dist/tests/yoga-plugin.test.js +125 -0
  39. package/dist/types/index.js +0 -0
  40. package/package.json +1 -1
@@ -0,0 +1,291 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { AbilityBuilder, PureAbility } from '@casl/ability';
3
+ import { defaultAbilityBuilder } from './ability';
4
+ /**
5
+ * Default user extractor from JWT payload
6
+ */
7
+ const defaultUserExtractor = (payload) => {
8
+ if (!payload.sub)
9
+ return undefined;
10
+ return {
11
+ id: payload.sub,
12
+ roles: payload.roles || [],
13
+ ...payload, // Include any additional claims
14
+ };
15
+ };
16
+ /**
17
+ * JWT middleware factory for GraphQL servers
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // In Nuxt Yoga
22
+ * export default defineNuxtConfig({
23
+ * yoga: {
24
+ * middleware: [
25
+ * createJWTMiddleware({
26
+ * enabled: true,
27
+ * secret: process.env.JWT_SECRET,
28
+ * optional: true // Don't fail if no token
29
+ * })
30
+ * ]
31
+ * }
32
+ * })
33
+ *
34
+ * // In Postgraphile
35
+ * const jwtPlugin = createPostgraphileJWTPlugin({
36
+ * secret: process.env.JWT_SECRET,
37
+ * extractUser: (payload) => ({
38
+ * id: payload.user_id,
39
+ * roles: payload.user_roles
40
+ * })
41
+ * })
42
+ * ```
43
+ */
44
+ export const createJWTMiddleware = (config = {}) => {
45
+ const { enabled = true, secret, publicKey, algorithms = ['HS256'], issuer, audience, headerName = 'authorization', tokenPrefix = 'Bearer ', optional = false, extractUser = defaultUserExtractor, maxAge, } = config;
46
+ // Validate configuration
47
+ if (enabled && !secret && !publicKey) {
48
+ throw new Error('JWT middleware requires either secret or publicKey');
49
+ }
50
+ return async (context, next) => {
51
+ // Skip if JWT is disabled
52
+ if (!enabled) {
53
+ return next();
54
+ }
55
+ try {
56
+ // Extract token from request headers
57
+ const authHeader = context.req?.headers?.get?.(headerName) ||
58
+ context.request?.headers?.get?.(headerName) ||
59
+ context.headers?.[headerName];
60
+ if (!authHeader) {
61
+ if (optional) {
62
+ return next();
63
+ }
64
+ throw new Error('No authorization header found');
65
+ }
66
+ // Remove token prefix
67
+ const token = authHeader.startsWith(tokenPrefix) ? authHeader.slice(tokenPrefix.length) : authHeader;
68
+ // Prepare verification options
69
+ const verifyOptions = {
70
+ algorithms,
71
+ ...(issuer && { issuer }),
72
+ ...(audience && { audience }),
73
+ ...(maxAge && { maxAge }),
74
+ };
75
+ // Verify and decode token
76
+ const secretOrPublicKey = publicKey || secret;
77
+ const payload = jwt.verify(token, secretOrPublicKey, verifyOptions);
78
+ // Extract user from payload
79
+ const user = extractUser(payload);
80
+ if (user) {
81
+ context.user = user;
82
+ // Store the raw payload for potential use
83
+ context.jwtPayload = payload;
84
+ }
85
+ // Continue to next middleware
86
+ return next();
87
+ }
88
+ catch (error) {
89
+ if (optional) {
90
+ // Log error in development
91
+ if (process.env.NODE_ENV === 'development') {
92
+ console.warn('JWT verification failed (optional):', error.message);
93
+ }
94
+ // Continue without user if optional
95
+ return next();
96
+ }
97
+ // Re-throw with more specific error messages
98
+ if (error.name === 'TokenExpiredError') {
99
+ throw new Error('Token has expired');
100
+ }
101
+ else if (error.name === 'JsonWebTokenError') {
102
+ throw new Error('Invalid token');
103
+ }
104
+ else if (error.name === 'NotBeforeError') {
105
+ throw new Error('Token not active yet');
106
+ }
107
+ throw error;
108
+ }
109
+ };
110
+ };
111
+ /**
112
+ * Create a JWT token with user data
113
+ */
114
+ export const createJWT = (user, config) => {
115
+ const { secret, expiresIn = '1h', issuer, audience, additionalClaims = {} } = config;
116
+ const payload = {
117
+ sub: user.id,
118
+ roles: user.roles || [],
119
+ ...additionalClaims,
120
+ };
121
+ const signOptions = {};
122
+ // Add optional fields only if they exist
123
+ if (issuer !== undefined)
124
+ signOptions.issuer = issuer;
125
+ if (audience !== undefined)
126
+ signOptions.audience = audience;
127
+ if (expiresIn !== undefined)
128
+ signOptions.expiresIn = expiresIn; // Type cast to avoid TS issues
129
+ return jwt.sign(payload, secret, signOptions);
130
+ };
131
+ /**
132
+ * Integration with CASL ability builder
133
+ */
134
+ export const createJWTAbilityBuilder = (config = {}) => {
135
+ return async (user) => {
136
+ // If user has direct permissions in JWT, use those
137
+ const jwtPermissions = user?.permissions;
138
+ if (jwtPermissions && Array.isArray(jwtPermissions)) {
139
+ // Build ability from JWT permissions
140
+ const { can, cannot, build } = new AbilityBuilder(PureAbility);
141
+ jwtPermissions.forEach((permission) => {
142
+ if (permission.inverted) {
143
+ cannot(permission.action, permission.subject, permission.conditions);
144
+ }
145
+ else {
146
+ can(permission.action, permission.subject, permission.conditions);
147
+ }
148
+ });
149
+ return build();
150
+ }
151
+ // Fall back to role-based abilities
152
+ return defaultAbilityBuilder(user);
153
+ };
154
+ };
155
+ /**
156
+ * Postgraphile-specific JWT plugin
157
+ */
158
+ export const createPostgraphileJWTPlugin = (config) => {
159
+ return {
160
+ name: 'JWTAuthPlugin',
161
+ version: '1.0.0',
162
+ // Hook into Postgraphile's context building
163
+ grafast: {
164
+ hooks: {
165
+ async context(ctx, build) {
166
+ const middleware = createJWTMiddleware(config);
167
+ // Create a simple context object that the middleware can work with
168
+ const context = {
169
+ req: ctx.req,
170
+ headers: ctx.req?.headers,
171
+ user: undefined,
172
+ jwtPayload: undefined,
173
+ };
174
+ // Run the JWT middleware
175
+ await middleware(context, async () => { });
176
+ // Add user to Postgraphile context
177
+ if (context.user) {
178
+ return { ...ctx, user: context.user, jwtPayload: context.jwtPayload };
179
+ }
180
+ return ctx;
181
+ },
182
+ },
183
+ },
184
+ };
185
+ };
186
+ /**
187
+ * Express/Koa middleware for REST endpoints
188
+ */
189
+ export const createHTTPJWTMiddleware = (config) => {
190
+ const jwtMiddleware = createJWTMiddleware(config);
191
+ // Express middleware
192
+ return async (req, res, next) => {
193
+ const context = {
194
+ req: {
195
+ headers: {
196
+ get: (name) => req.headers[name],
197
+ },
198
+ },
199
+ headers: req.headers,
200
+ user: undefined,
201
+ jwtPayload: undefined,
202
+ };
203
+ try {
204
+ await jwtMiddleware(context, async () => { });
205
+ req.user = context.user;
206
+ req.jwtPayload = context.jwtPayload;
207
+ next();
208
+ }
209
+ catch (error) {
210
+ if (config.optional) {
211
+ next();
212
+ }
213
+ else {
214
+ res.status(401).json({
215
+ error: error.message,
216
+ code: 'UNAUTHORIZED',
217
+ });
218
+ }
219
+ }
220
+ };
221
+ };
222
+ /**
223
+ * Refresh token utilities
224
+ */
225
+ export const refreshTokenUtils = {
226
+ /**
227
+ * Create access and refresh tokens
228
+ */
229
+ createTokenPair: (user, config) => {
230
+ const { accessSecret, refreshSecret, accessExpiresIn = '15m', refreshExpiresIn = '7d' } = config;
231
+ const accessPayload = {
232
+ sub: user.id,
233
+ roles: user.roles,
234
+ type: 'access',
235
+ };
236
+ const refreshPayload = {
237
+ sub: user.id,
238
+ type: 'refresh',
239
+ };
240
+ // Create access token with proper options
241
+ const accessOptions = {};
242
+ if (accessExpiresIn) {
243
+ accessOptions.expiresIn = accessExpiresIn; // Type cast to avoid TS issues
244
+ }
245
+ const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
246
+ // Create refresh token with proper options
247
+ const refreshOptions = {};
248
+ if (refreshExpiresIn) {
249
+ refreshOptions.expiresIn = refreshExpiresIn; // Type cast to avoid TS issues
250
+ }
251
+ const refreshToken = jwt.sign(refreshPayload, refreshSecret, refreshOptions);
252
+ return { accessToken, refreshToken };
253
+ },
254
+ /**
255
+ * Verify refresh token and create new access token
256
+ */
257
+ refreshAccessToken: async (refreshToken, config) => {
258
+ const { accessSecret, refreshSecret, getUserById, accessExpiresIn = '15m' } = config;
259
+ try {
260
+ // Verify refresh token
261
+ const payload = jwt.verify(refreshToken, refreshSecret);
262
+ if (payload.type !== 'refresh') {
263
+ throw new Error('Invalid token type');
264
+ }
265
+ // Get fresh user data
266
+ const user = await getUserById(payload.sub);
267
+ if (!user) {
268
+ throw new Error('User not found');
269
+ }
270
+ // Create new access token
271
+ const accessPayload = {
272
+ sub: user.id,
273
+ roles: user.roles,
274
+ type: 'access',
275
+ };
276
+ // Create access token with proper options
277
+ const accessOptions = {};
278
+ if (accessExpiresIn) {
279
+ accessOptions.expiresIn = accessExpiresIn; // Type cast to avoid TS issues
280
+ }
281
+ const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
282
+ return { accessToken, user };
283
+ }
284
+ catch (error) {
285
+ if (error.name === 'TokenExpiredError') {
286
+ throw new Error('Refresh token expired');
287
+ }
288
+ throw error;
289
+ }
290
+ },
291
+ };
@@ -0,0 +1,80 @@
1
+ import { extendSchema, gql } from 'postgraphile/utils';
2
+ import { createAbility } from './ability';
3
+ /**
4
+ * PostGraphile plugin for CASL authorization
5
+ * @public
6
+ */
7
+ export const pglCaslPlugin = extendSchema(build => {
8
+ const { grafast: { constant, object, sideEffect }, } = build;
9
+ return {
10
+ typeDefs: gql `
11
+ input CreateAbilityInput {
12
+ userId: String!
13
+ roles: [String!]
14
+ }
15
+
16
+ type AbilityResponse {
17
+ success: Boolean!
18
+ ability: JSON
19
+ message: String
20
+ }
21
+
22
+ type SecretData {
23
+ id: String!
24
+ content: String!
25
+ }
26
+
27
+ extend type Query {
28
+ getSecretData: SecretData
29
+ }
30
+
31
+ extend type Mutation {
32
+ createAbility(input: CreateAbilityInput!): AbilityResponse!
33
+ }
34
+ `,
35
+ objects: {
36
+ Query: {
37
+ plans: {
38
+ async getSecretData() {
39
+ // TODO: This should be protected by CASL
40
+ // const $ability = context<Context>().get('ability')
41
+ // if (!$ability.can('read', 'SecretData')) {
42
+ // throw new Error('Access denied')
43
+ // }
44
+ return object({
45
+ id: constant('123'),
46
+ content: constant('This is protected content'),
47
+ });
48
+ },
49
+ },
50
+ },
51
+ Mutation: {
52
+ plans: {
53
+ async createAbility(_plan, fieldArgs) {
54
+ const $input = fieldArgs.getRaw().input;
55
+ const $userId = $input.userId;
56
+ const $roles = $input.roles;
57
+ return sideEffect([$userId, $roles], async ([userId, roles]) => {
58
+ // Make this async
59
+ try {
60
+ const ability = await createAbility({ id: userId, roles }); // Await here
61
+ return {
62
+ success: true,
63
+ ability: ability.rules,
64
+ message: 'Ability created successfully',
65
+ };
66
+ }
67
+ catch (error) {
68
+ return {
69
+ success: false,
70
+ ability: null,
71
+ message: error instanceof Error ? error.message : 'Unknown error occurred',
72
+ };
73
+ }
74
+ });
75
+ },
76
+ },
77
+ },
78
+ },
79
+ };
80
+ });
@@ -0,0 +1,32 @@
1
+ import { createAbility } from './ability';
2
+ import { createCaslMiddleware } from './graphql';
3
+ const getLoggedInUser = () => {
4
+ // Mock user for demonstration purposes
5
+ return { id: '1', roles: ['editor'] };
6
+ };
7
+ export const yogaCaslPlugin = {
8
+ onContextBuilding: async ({ context, extendContext }) => {
9
+ // Make this async
10
+ const user = getLoggedInUser();
11
+ const ability = await createAbility(user); // Await here
12
+ extendContext({ ability, user });
13
+ },
14
+ };
15
+ /**
16
+ * Create a GraphQL Yoga plugin for CASL authorization
17
+ * Note: This is a placeholder for future implementation
18
+ *
19
+ * @param options - CASL middleware configuration options
20
+ * @returns Yoga plugin
21
+ * @public
22
+ */
23
+ export const createYogaPlugin = (options = {}) => {
24
+ const middleware = createCaslMiddleware(options);
25
+ return {
26
+ onExecute: async ({ args }) => {
27
+ // TODO: Implement Yoga plugin
28
+ // This would integrate with GraphQL Yoga's plugin system
29
+ console.log('Yoga plugin not yet implemented');
30
+ },
31
+ };
32
+ };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.57.0"
9
+ }
10
+ ]
11
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ability.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.test.d.ts","sourceRoot":"","sources":["../../tests/ability.test.ts"],"names":[],"mappings":""}