@stonecrop/casl-middleware 0.30.0 → 0.32.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/dist/casl-middleware.d.ts +15 -0
- package/dist/casl-middleware.js +3328 -2650
- package/dist/casl-middleware.js.map +1 -1
- package/dist/tsdoc-metadata.json +1 -1
- package/package.json +23 -22
- package/dist/casl-middleware.tsbuildinfo +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -3
- package/dist/src/middleware/ability.d.ts +0 -55
- package/dist/src/middleware/ability.d.ts.map +0 -1
- package/dist/src/middleware/ability.js +0 -139
- package/dist/src/middleware/graphql.d.ts +0 -11
- package/dist/src/middleware/graphql.d.ts.map +0 -1
- package/dist/src/middleware/graphql.js +0 -120
- package/dist/src/middleware/introspection.d.ts +0 -71
- package/dist/src/middleware/introspection.d.ts.map +0 -1
- package/dist/src/middleware/introspection.js +0 -169
- package/dist/src/middleware/jwt.d.ts +0 -114
- package/dist/src/middleware/jwt.d.ts.map +0 -1
- package/dist/src/middleware/jwt.js +0 -302
- package/dist/src/middleware/postgraphile.d.ts +0 -7
- package/dist/src/middleware/postgraphile.d.ts.map +0 -1
- package/dist/src/middleware/postgraphile.js +0 -79
- package/dist/src/middleware/yoga.d.ts +0 -15
- package/dist/src/middleware/yoga.d.ts.map +0 -1
- package/dist/src/middleware/yoga.js +0 -30
- package/dist/src/types/index.d.ts +0 -114
- package/dist/src/types/index.d.ts.map +0 -1
- package/dist/src/types/index.js +0 -0
- package/src/index.ts +0 -15
- package/src/middleware/ability.ts +0 -197
- package/src/middleware/graphql.ts +0 -157
- package/src/middleware/introspection.ts +0 -258
- package/src/middleware/jwt.ts +0 -405
- package/src/middleware/postgraphile.ts +0 -89
- package/src/middleware/yoga.ts +0 -36
- package/src/types/index.ts +0 -133
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { GraphQLError } from 'graphql';
|
|
2
|
-
/**
|
|
3
|
-
* Middleware to restrict GraphQL introspection based on user permissions
|
|
4
|
-
*/
|
|
5
|
-
export const createIntrospectionMiddleware = (config = {}) => {
|
|
6
|
-
const { enabled = process.env.NODE_ENV !== 'production', allowedRoles, allowAnonymous = false, customCheck, typePermissions = {}, fieldPermissions = {}, } = config;
|
|
7
|
-
return async (resolve, root, args, context, info) => {
|
|
8
|
-
// Check if this is an introspection query
|
|
9
|
-
const isIntrospection = info.fieldName === '__schema' ||
|
|
10
|
-
info.fieldName === '__type' ||
|
|
11
|
-
info.parentType?.name === '__Schema' ||
|
|
12
|
-
info.parentType?.name === '__Type';
|
|
13
|
-
if (!isIntrospection) {
|
|
14
|
-
// Not an introspection query, continue normally
|
|
15
|
-
return resolve(root, args, context, info);
|
|
16
|
-
}
|
|
17
|
-
// Check if introspection is enabled
|
|
18
|
-
if (!enabled) {
|
|
19
|
-
throw new GraphQLError('Introspection is disabled');
|
|
20
|
-
}
|
|
21
|
-
// Custom check function
|
|
22
|
-
if (customCheck) {
|
|
23
|
-
const allowed = await customCheck(context);
|
|
24
|
-
if (!allowed) {
|
|
25
|
-
throw new GraphQLError('Introspection not allowed');
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// Check authentication
|
|
29
|
-
if (!context.user && !allowAnonymous) {
|
|
30
|
-
throw new GraphQLError('Authentication required for introspection');
|
|
31
|
-
}
|
|
32
|
-
// Check role-based access
|
|
33
|
-
if (allowedRoles && allowedRoles.length > 0) {
|
|
34
|
-
const userRoles = context.user?.roles || [];
|
|
35
|
-
const hasAllowedRole = allowedRoles.some(role => userRoles.includes(role));
|
|
36
|
-
if (!hasAllowedRole) {
|
|
37
|
-
throw new GraphQLError('Insufficient permissions for introspection');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Check CASL-based permissions
|
|
41
|
-
if (context.ability) {
|
|
42
|
-
// Check if user can read schema
|
|
43
|
-
if (!context.ability.can('read', '__Schema')) {
|
|
44
|
-
throw new GraphQLError('Permission denied for schema introspection');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Get the result
|
|
48
|
-
let result = await resolve(root, args, context, info);
|
|
49
|
-
// Filter the result based on permissions
|
|
50
|
-
if (result && (typePermissions || fieldPermissions)) {
|
|
51
|
-
result = filterIntrospectionResult(result, context, {
|
|
52
|
-
typePermissions,
|
|
53
|
-
fieldPermissions,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
};
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Filter introspection results based on user permissions
|
|
61
|
-
*/
|
|
62
|
-
function filterIntrospectionResult(result, context, config) {
|
|
63
|
-
if (!context.ability)
|
|
64
|
-
return result;
|
|
65
|
-
// Filter __schema result
|
|
66
|
-
if (result && result.types) {
|
|
67
|
-
result.types = result.types.filter((type) => {
|
|
68
|
-
// Check if user has permission to see this type
|
|
69
|
-
const permission = config.typePermissions?.[type.name];
|
|
70
|
-
if (permission) {
|
|
71
|
-
return context.ability.can(permission.action, permission.subject);
|
|
72
|
-
}
|
|
73
|
-
return true; // Show types without specific permissions
|
|
74
|
-
});
|
|
75
|
-
// Filter fields within types
|
|
76
|
-
result.types.forEach((type) => {
|
|
77
|
-
if (type.fields) {
|
|
78
|
-
type.fields = type.fields.filter((field) => {
|
|
79
|
-
const fieldKey = `${type.name}.${field.name}`;
|
|
80
|
-
const permission = config.fieldPermissions?.[fieldKey];
|
|
81
|
-
if (permission) {
|
|
82
|
-
return context.ability.can(permission.action, permission.subject);
|
|
83
|
-
}
|
|
84
|
-
return true;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// Filter __type result
|
|
90
|
-
if (result && result.fields) {
|
|
91
|
-
const typeName = result.name;
|
|
92
|
-
result.fields = result.fields.filter((field) => {
|
|
93
|
-
const fieldKey = `${typeName}.${field.name}`;
|
|
94
|
-
const permission = config.fieldPermissions?.[fieldKey];
|
|
95
|
-
if (permission) {
|
|
96
|
-
return context.ability.can(permission.action, permission.subject);
|
|
97
|
-
}
|
|
98
|
-
return true;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return result;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Postgraphile plugin for introspection control
|
|
105
|
-
*/
|
|
106
|
-
export const createPostgraphileIntrospectionPlugin = (config) => {
|
|
107
|
-
return {
|
|
108
|
-
name: 'IntrospectionControlPlugin',
|
|
109
|
-
version: '1.0.0',
|
|
110
|
-
// Disable introspection in GraphiQL based on config
|
|
111
|
-
grafast: {
|
|
112
|
-
hooks: {
|
|
113
|
-
GraphQLSchema(schema) {
|
|
114
|
-
if (!config.enabled) {
|
|
115
|
-
// Remove introspection from schema
|
|
116
|
-
// This is a simplified approach - real implementation would be more complex
|
|
117
|
-
console.warn('Introspection control in Postgraphile requires custom implementation');
|
|
118
|
-
}
|
|
119
|
-
return schema;
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* Utility to create ability rules for introspection
|
|
127
|
-
*/
|
|
128
|
-
export const createIntrospectionAbilityRules = (user) => {
|
|
129
|
-
const rules = [];
|
|
130
|
-
if (!user) {
|
|
131
|
-
// Anonymous users cannot introspect
|
|
132
|
-
return rules;
|
|
133
|
-
}
|
|
134
|
-
const roles = user.roles || [];
|
|
135
|
-
// Admins can introspect everything
|
|
136
|
-
if (roles.includes('admin')) {
|
|
137
|
-
rules.push({ action: 'read', subject: '__Schema' });
|
|
138
|
-
rules.push({ action: 'read', subject: '__Type' });
|
|
139
|
-
return rules;
|
|
140
|
-
}
|
|
141
|
-
// Developers can introspect
|
|
142
|
-
if (roles.includes('developer')) {
|
|
143
|
-
rules.push({ action: 'read', subject: '__Schema' });
|
|
144
|
-
rules.push({ action: 'read', subject: '__Type' });
|
|
145
|
-
return rules;
|
|
146
|
-
}
|
|
147
|
-
// Regular users get limited introspection
|
|
148
|
-
if (roles.includes('user')) {
|
|
149
|
-
// They can see the schema but not all types
|
|
150
|
-
rules.push({ action: 'read', subject: '__Schema' });
|
|
151
|
-
// Specific types they can see would be added here
|
|
152
|
-
}
|
|
153
|
-
return rules;
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Example: Combine introspection with CASL middleware
|
|
157
|
-
*/
|
|
158
|
-
export const createSecureGraphQLMiddleware = (options) => {
|
|
159
|
-
const middlewares = [];
|
|
160
|
-
// Add introspection control
|
|
161
|
-
if (options.introspection) {
|
|
162
|
-
middlewares.push(createIntrospectionMiddleware(options.introspection));
|
|
163
|
-
}
|
|
164
|
-
// Combine all middlewares
|
|
165
|
-
return (resolve, root, args, context, info) => {
|
|
166
|
-
const chain = middlewares.reduceRight((next, middleware) => () => middleware(next, root, args, context, info), () => resolve(root, args, context, info));
|
|
167
|
-
return chain();
|
|
168
|
-
};
|
|
169
|
-
};
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import jwt from 'jsonwebtoken';
|
|
2
|
-
import type { Context, User } from '../types';
|
|
3
|
-
export interface JWTConfig {
|
|
4
|
-
enabled?: boolean;
|
|
5
|
-
secret?: string;
|
|
6
|
-
publicKey?: string;
|
|
7
|
-
algorithms?: jwt.Algorithm[];
|
|
8
|
-
issuer?: string;
|
|
9
|
-
audience?: string;
|
|
10
|
-
extractUser?: (payload: any) => User | undefined;
|
|
11
|
-
headerName?: string;
|
|
12
|
-
tokenPrefix?: string;
|
|
13
|
-
optional?: boolean;
|
|
14
|
-
maxAge?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface JWTPayload extends jwt.JwtPayload {
|
|
17
|
-
sub?: string;
|
|
18
|
-
roles?: string[];
|
|
19
|
-
permissions?: Array<{
|
|
20
|
-
action: string;
|
|
21
|
-
subject: string;
|
|
22
|
-
conditions?: any;
|
|
23
|
-
}>;
|
|
24
|
-
[key: string]: any;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* JWT middleware factory for GraphQL servers
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```typescript
|
|
31
|
-
* // In Nuxt Yoga
|
|
32
|
-
* export default defineNuxtConfig({
|
|
33
|
-
* yoga: {
|
|
34
|
-
* middleware: [
|
|
35
|
-
* createJWTMiddleware({
|
|
36
|
-
* enabled: true,
|
|
37
|
-
* secret: process.env.JWT_SECRET,
|
|
38
|
-
* optional: true // Don't fail if no token
|
|
39
|
-
* })
|
|
40
|
-
* ]
|
|
41
|
-
* }
|
|
42
|
-
* })
|
|
43
|
-
*
|
|
44
|
-
* // In Postgraphile
|
|
45
|
-
* const jwtPlugin = createPostgraphileJWTPlugin({
|
|
46
|
-
* secret: process.env.JWT_SECRET,
|
|
47
|
-
* extractUser: (payload) => ({
|
|
48
|
-
* id: payload.user_id,
|
|
49
|
-
* roles: payload.user_roles
|
|
50
|
-
* })
|
|
51
|
-
* })
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export declare const createJWTMiddleware: (config?: JWTConfig) => (context: Context, next: () => Promise<any>) => Promise<any>;
|
|
55
|
-
/**
|
|
56
|
-
* Create a JWT token with user data
|
|
57
|
-
*/
|
|
58
|
-
export declare const createJWT: (user: User, config: {
|
|
59
|
-
secret: string;
|
|
60
|
-
expiresIn?: jwt.SignOptions["expiresIn"];
|
|
61
|
-
issuer?: string;
|
|
62
|
-
audience?: string;
|
|
63
|
-
additionalClaims?: Record<string, any>;
|
|
64
|
-
}) => string;
|
|
65
|
-
/**
|
|
66
|
-
* Integration with CASL ability builder
|
|
67
|
-
*/
|
|
68
|
-
export declare const createJWTAbilityBuilder: (_config?: JWTConfig) => (user?: User) => Promise<import("./ability").AppAbility>;
|
|
69
|
-
/**
|
|
70
|
-
* Postgraphile-specific JWT plugin
|
|
71
|
-
*/
|
|
72
|
-
export declare const createPostgraphileJWTPlugin: (config: JWTConfig) => {
|
|
73
|
-
name: string;
|
|
74
|
-
version: string;
|
|
75
|
-
grafast: {
|
|
76
|
-
hooks: {
|
|
77
|
-
context(ctx: any, _build: any): Promise<any>;
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* Express/Koa middleware for REST endpoints
|
|
83
|
-
*/
|
|
84
|
-
export declare const createHTTPJWTMiddleware: (config: JWTConfig) => (req: any, res: any, next: any) => Promise<void>;
|
|
85
|
-
/**
|
|
86
|
-
* Refresh token utilities
|
|
87
|
-
*/
|
|
88
|
-
export declare const refreshTokenUtils: {
|
|
89
|
-
/**
|
|
90
|
-
* Create access and refresh tokens
|
|
91
|
-
*/
|
|
92
|
-
createTokenPair: (user: User, config: {
|
|
93
|
-
accessSecret: string;
|
|
94
|
-
refreshSecret: string;
|
|
95
|
-
accessExpiresIn?: jwt.SignOptions["expiresIn"];
|
|
96
|
-
refreshExpiresIn?: jwt.SignOptions["expiresIn"];
|
|
97
|
-
}) => {
|
|
98
|
-
accessToken: string;
|
|
99
|
-
refreshToken: string;
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Verify refresh token and create new access token
|
|
103
|
-
*/
|
|
104
|
-
refreshAccessToken: (refreshToken: string, config: {
|
|
105
|
-
accessSecret: string;
|
|
106
|
-
refreshSecret: string;
|
|
107
|
-
getUserById: (id: string) => Promise<User | null>;
|
|
108
|
-
accessExpiresIn?: jwt.SignOptions["expiresIn"];
|
|
109
|
-
}) => Promise<{
|
|
110
|
-
accessToken: string;
|
|
111
|
-
user: User;
|
|
112
|
-
}>;
|
|
113
|
-
};
|
|
114
|
-
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,cAAc,CAAA;AAE9B,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAG7C,MAAM,WAAW,SAAS;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,GAAG,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,GAAG,SAAS,CAAA;IAChD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,UAAW,SAAQ,GAAG,CAAC,UAAU;IACjD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC;QACnB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;QACf,UAAU,CAAC,EAAE,GAAG,CAAA;KAChB,CAAC,CAAA;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAClB;AAeD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,mBAAmB,GAAI,SAAQ,SAAc,MAoB3C,SAAS,OAAO,EAAE,MAAM,MAAM,OAAO,CAAC,GAAG,CAAC,iBAwExD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,GACrB,MAAM,IAAI,EACV,QAAQ;IACP,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACtC,KACC,MAiBF,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAS,SAAc,MAChD,OAAO,IAAI,4CAsBzB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,2BAA2B,GAAI,QAAQ,SAAS;;;;;yBAQtC,GAAG,UAAU,GAAG;;;CAwBtC,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAAI,QAAQ,SAAS,MAI1C,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,GAAG,kBA4B3C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,iBAAiB;IAC7B;;OAEG;4BAEI,IAAI,UACF;QACP,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,eAAe,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;QAC9C,gBAAgB,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;KAC/C;;;;IAkCF;;OAEG;uCAEY,MAAM,UACZ;QACP,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,WAAW,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;QACjD,eAAe,CAAC,EAAE,GAAG,CAAC,WAAW,CAAC,WAAW,CAAC,CAAA;KAC9C;;;;CAgDF,CAAA"}
|
|
@@ -1,302 +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 decoded = jwt.verify(token, secretOrPublicKey, verifyOptions);
|
|
78
|
-
if (typeof decoded === 'string' || decoded == null) {
|
|
79
|
-
throw new Error('Invalid JWT payload: expected object');
|
|
80
|
-
}
|
|
81
|
-
const payload = decoded;
|
|
82
|
-
// Extract user from payload
|
|
83
|
-
const user = extractUser(payload);
|
|
84
|
-
if (user) {
|
|
85
|
-
context.user = user;
|
|
86
|
-
// Store the raw payload for potential use
|
|
87
|
-
context.jwtPayload = payload;
|
|
88
|
-
}
|
|
89
|
-
// Continue to next middleware
|
|
90
|
-
return next();
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
if (optional) {
|
|
94
|
-
// Log error in development
|
|
95
|
-
if (process.env.NODE_ENV === 'development') {
|
|
96
|
-
console.warn('JWT verification failed (optional):', error.message);
|
|
97
|
-
}
|
|
98
|
-
// Continue without user if optional
|
|
99
|
-
return next();
|
|
100
|
-
}
|
|
101
|
-
// Re-throw with more specific error messages
|
|
102
|
-
if (error.name === 'TokenExpiredError') {
|
|
103
|
-
throw new Error('Token has expired', { cause: error });
|
|
104
|
-
}
|
|
105
|
-
else if (error.name === 'JsonWebTokenError') {
|
|
106
|
-
throw new Error('Invalid token', { cause: error });
|
|
107
|
-
}
|
|
108
|
-
else if (error.name === 'NotBeforeError') {
|
|
109
|
-
throw new Error('Token not active yet', { cause: error });
|
|
110
|
-
}
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
/**
|
|
116
|
-
* Create a JWT token with user data
|
|
117
|
-
*/
|
|
118
|
-
export const createJWT = (user, config) => {
|
|
119
|
-
const { secret, expiresIn = '1h', issuer, audience, additionalClaims = {} } = config;
|
|
120
|
-
const payload = {
|
|
121
|
-
sub: user.id,
|
|
122
|
-
roles: user.roles || [],
|
|
123
|
-
...additionalClaims,
|
|
124
|
-
};
|
|
125
|
-
const signOptions = {};
|
|
126
|
-
// Add optional fields only if they exist
|
|
127
|
-
if (issuer !== undefined)
|
|
128
|
-
signOptions.issuer = issuer;
|
|
129
|
-
if (audience !== undefined)
|
|
130
|
-
signOptions.audience = audience;
|
|
131
|
-
if (expiresIn !== undefined)
|
|
132
|
-
signOptions.expiresIn = expiresIn;
|
|
133
|
-
return jwt.sign(payload, secret, signOptions);
|
|
134
|
-
};
|
|
135
|
-
/**
|
|
136
|
-
* Integration with CASL ability builder
|
|
137
|
-
*/
|
|
138
|
-
export const createJWTAbilityBuilder = (_config = {}) => {
|
|
139
|
-
return async (user) => {
|
|
140
|
-
// If user has direct permissions in JWT, use those
|
|
141
|
-
const jwtPermissions = user?.permissions;
|
|
142
|
-
if (jwtPermissions && Array.isArray(jwtPermissions)) {
|
|
143
|
-
// Build ability from JWT permissions
|
|
144
|
-
const { can, cannot, build } = new AbilityBuilder(PureAbility);
|
|
145
|
-
jwtPermissions.forEach((permission) => {
|
|
146
|
-
if (permission.inverted) {
|
|
147
|
-
cannot(permission.action, permission.subject, permission.conditions);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
can(permission.action, permission.subject, permission.conditions);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
return build();
|
|
154
|
-
}
|
|
155
|
-
// Fall back to role-based abilities
|
|
156
|
-
return defaultAbilityBuilder(user);
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
/**
|
|
160
|
-
* Postgraphile-specific JWT plugin
|
|
161
|
-
*/
|
|
162
|
-
export const createPostgraphileJWTPlugin = (config) => {
|
|
163
|
-
return {
|
|
164
|
-
name: 'JWTAuthPlugin',
|
|
165
|
-
version: '1.0.0',
|
|
166
|
-
// Hook into Postgraphile's context building
|
|
167
|
-
grafast: {
|
|
168
|
-
hooks: {
|
|
169
|
-
async context(ctx, _build) {
|
|
170
|
-
const middleware = createJWTMiddleware(config);
|
|
171
|
-
// Create a simple context object that the middleware can work with
|
|
172
|
-
const context = {
|
|
173
|
-
req: ctx.req,
|
|
174
|
-
headers: ctx.req?.headers,
|
|
175
|
-
user: undefined,
|
|
176
|
-
jwtPayload: undefined,
|
|
177
|
-
};
|
|
178
|
-
// Run the JWT middleware
|
|
179
|
-
await middleware(context, async () => { });
|
|
180
|
-
// Add user to Postgraphile context
|
|
181
|
-
if (context.user) {
|
|
182
|
-
return { ...ctx, user: context.user, jwtPayload: context.jwtPayload };
|
|
183
|
-
}
|
|
184
|
-
return ctx;
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
};
|
|
190
|
-
/**
|
|
191
|
-
* Express/Koa middleware for REST endpoints
|
|
192
|
-
*/
|
|
193
|
-
export const createHTTPJWTMiddleware = (config) => {
|
|
194
|
-
const jwtMiddleware = createJWTMiddleware(config);
|
|
195
|
-
// Express middleware
|
|
196
|
-
return async (req, res, next) => {
|
|
197
|
-
const context = {
|
|
198
|
-
req: {
|
|
199
|
-
headers: {
|
|
200
|
-
get: (name) => req.headers[name],
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
headers: req.headers,
|
|
204
|
-
user: undefined,
|
|
205
|
-
jwtPayload: undefined,
|
|
206
|
-
};
|
|
207
|
-
try {
|
|
208
|
-
await jwtMiddleware(context, async () => { });
|
|
209
|
-
req.user = context.user;
|
|
210
|
-
req.jwtPayload = context.jwtPayload;
|
|
211
|
-
next();
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (config.optional) {
|
|
215
|
-
next();
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
res.status(401).json({
|
|
219
|
-
error: error.message,
|
|
220
|
-
code: 'UNAUTHORIZED',
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
};
|
|
226
|
-
/**
|
|
227
|
-
* Refresh token utilities
|
|
228
|
-
*/
|
|
229
|
-
export const refreshTokenUtils = {
|
|
230
|
-
/**
|
|
231
|
-
* Create access and refresh tokens
|
|
232
|
-
*/
|
|
233
|
-
createTokenPair: (user, config) => {
|
|
234
|
-
const { accessSecret, refreshSecret, accessExpiresIn = '15m', refreshExpiresIn = '7d' } = config;
|
|
235
|
-
const accessPayload = {
|
|
236
|
-
sub: user.id,
|
|
237
|
-
roles: user.roles,
|
|
238
|
-
type: 'access',
|
|
239
|
-
};
|
|
240
|
-
const refreshPayload = {
|
|
241
|
-
sub: user.id,
|
|
242
|
-
type: 'refresh',
|
|
243
|
-
};
|
|
244
|
-
// Create access token with proper options
|
|
245
|
-
const accessOptions = {};
|
|
246
|
-
if (accessExpiresIn) {
|
|
247
|
-
accessOptions.expiresIn = accessExpiresIn;
|
|
248
|
-
}
|
|
249
|
-
const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
|
|
250
|
-
// Create refresh token with proper options
|
|
251
|
-
const refreshOptions = {};
|
|
252
|
-
if (refreshExpiresIn) {
|
|
253
|
-
refreshOptions.expiresIn = refreshExpiresIn;
|
|
254
|
-
}
|
|
255
|
-
const refreshToken = jwt.sign(refreshPayload, refreshSecret, refreshOptions);
|
|
256
|
-
return { accessToken, refreshToken };
|
|
257
|
-
},
|
|
258
|
-
/**
|
|
259
|
-
* Verify refresh token and create new access token
|
|
260
|
-
*/
|
|
261
|
-
refreshAccessToken: async (refreshToken, config) => {
|
|
262
|
-
const { accessSecret, refreshSecret, getUserById, accessExpiresIn = '15m' } = config;
|
|
263
|
-
try {
|
|
264
|
-
// Verify refresh token
|
|
265
|
-
const decoded = jwt.verify(refreshToken, refreshSecret);
|
|
266
|
-
if (typeof decoded === 'string' || decoded == null) {
|
|
267
|
-
throw new Error('Invalid refresh token payload: expected object');
|
|
268
|
-
}
|
|
269
|
-
const payload = decoded;
|
|
270
|
-
if (payload.type !== 'refresh') {
|
|
271
|
-
throw new Error('Invalid token type');
|
|
272
|
-
}
|
|
273
|
-
// Get fresh user data
|
|
274
|
-
if (!payload.sub) {
|
|
275
|
-
throw new Error('Invalid refresh token payload: missing subject');
|
|
276
|
-
}
|
|
277
|
-
const user = await getUserById(payload.sub);
|
|
278
|
-
if (!user) {
|
|
279
|
-
throw new Error('User not found');
|
|
280
|
-
}
|
|
281
|
-
// Create new access token
|
|
282
|
-
const accessPayload = {
|
|
283
|
-
sub: user.id,
|
|
284
|
-
roles: user.roles,
|
|
285
|
-
type: 'access',
|
|
286
|
-
};
|
|
287
|
-
// Create access token with proper options
|
|
288
|
-
const accessOptions = {};
|
|
289
|
-
if (accessExpiresIn) {
|
|
290
|
-
accessOptions.expiresIn = accessExpiresIn;
|
|
291
|
-
}
|
|
292
|
-
const accessToken = jwt.sign(accessPayload, accessSecret, accessOptions);
|
|
293
|
-
return { accessToken, user };
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
if (error.name === 'TokenExpiredError') {
|
|
297
|
-
throw new Error('Refresh token expired', { cause: error });
|
|
298
|
-
}
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postgraphile.d.ts","sourceRoot":"","sources":["../../../src/middleware/postgraphile.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAI5D;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,cAAc,CAAC,MA8EzC,CAAA"}
|
|
@@ -1,79 +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 $userId = fieldArgs.getRaw().input.userId;
|
|
55
|
-
const $roles = fieldArgs.getRaw().input.roles;
|
|
56
|
-
return sideEffect([$userId, $roles], async ([userId, roles]) => {
|
|
57
|
-
// Make this async
|
|
58
|
-
try {
|
|
59
|
-
const ability = await createAbility({ id: userId, roles }); // Await here
|
|
60
|
-
return {
|
|
61
|
-
success: true,
|
|
62
|
-
ability: ability.rules,
|
|
63
|
-
message: 'Ability created successfully',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
catch (error) {
|
|
67
|
-
return {
|
|
68
|
-
success: false,
|
|
69
|
-
ability: null,
|
|
70
|
-
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from 'graphql-yoga';
|
|
2
|
-
import type { Context, MiddlewareOptions } from '../types';
|
|
3
|
-
export declare const yogaCaslPlugin: Plugin<Context>;
|
|
4
|
-
/**
|
|
5
|
-
* Create a GraphQL Yoga plugin for CASL authorization
|
|
6
|
-
* Note: This is a placeholder for future implementation
|
|
7
|
-
*
|
|
8
|
-
* @param options - CASL middleware configuration options
|
|
9
|
-
* @returns Yoga plugin
|
|
10
|
-
* @public
|
|
11
|
-
*/
|
|
12
|
-
export declare const createYogaPlugin: (_options?: MiddlewareOptions) => {
|
|
13
|
-
onExecute: (_: any) => Promise<void>;
|
|
14
|
-
};
|
|
15
|
-
//# sourceMappingURL=yoga.d.ts.map
|