@stonecrop/casl-middleware 0.14.0 → 0.15.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.
- package/package.json +23 -23
- package/dist/index.js +0 -3
- package/dist/middleware/ability.js +0 -139
- package/dist/middleware/graphql.js +0 -120
- package/dist/middleware/introspection.js +0 -169
- package/dist/middleware/jwt.js +0 -291
- package/dist/middleware/postgraphile.js +0 -80
- package/dist/middleware/yoga.js +0 -32
- package/dist/src/tsdoc-metadata.json +0 -11
- package/dist/tests/ability.test.d.ts +0 -2
- package/dist/tests/ability.test.d.ts.map +0 -1
- package/dist/tests/ability.test.js +0 -402
- package/dist/tests/helpers/test-utils.d.ts +0 -46
- package/dist/tests/helpers/test-utils.d.ts.map +0 -1
- package/dist/tests/helpers/test-utils.js +0 -92
- package/dist/tests/introspection.test.d.ts +0 -2
- package/dist/tests/introspection.test.d.ts.map +0 -1
- package/dist/tests/introspection.test.js +0 -368
- package/dist/tests/jwt.test.d.ts +0 -2
- package/dist/tests/jwt.test.d.ts.map +0 -1
- package/dist/tests/jwt.test.js +0 -700
- package/dist/tests/middleware.test.d.ts +0 -2
- package/dist/tests/middleware.test.d.ts.map +0 -1
- package/dist/tests/middleware.test.js +0 -184
- package/dist/tests/postgraphile-plugin.test.d.ts +0 -2
- package/dist/tests/postgraphile-plugin.test.d.ts.map +0 -1
- package/dist/tests/postgraphile-plugin.test.js +0 -56
- package/dist/tests/setup.d.ts +0 -2
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -11
- package/dist/tests/types.test.d.ts +0 -2
- package/dist/tests/types.test.d.ts.map +0 -1
- package/dist/tests/types.test.js +0 -143
- package/dist/tests/user-roles.test.d.ts +0 -2
- package/dist/tests/user-roles.test.d.ts.map +0 -1
- package/dist/tests/user-roles.test.js +0 -157
- package/dist/tests/yoga-plugin.test.d.ts +0 -2
- package/dist/tests/yoga-plugin.test.d.ts.map +0 -1
- package/dist/tests/yoga-plugin.test.js +0 -125
- package/dist/types/index.js +0 -0
package/dist/middleware/jwt.js
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
});
|
package/dist/middleware/yoga.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ability.test.d.ts","sourceRoot":"","sources":["../../tests/ability.test.ts"],"names":[],"mappings":""}
|