This repository was archived by the owner on May 28, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathauth.js
More file actions
100 lines (87 loc) · 4.16 KB
/
auth.js
File metadata and controls
100 lines (87 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/*
* This logic and code was implemented from the following resources:
* - https://www.apollographql.com/docs/apollo-server/security/authentication/#with-custom-directives
* - https://the-guild.dev/graphql/tools/docs/schema-directives#enforcing-access-permissions
*/
import { getDirective, MapperKind, mapSchema } from "@graphql-tools/utils";
import { defaultFieldResolver } from "graphql";
// The header we parse to get the current role
export const USER_HEADER = "x-user-role";
export const AUTH_N_DIRECTIVE_NAME = "authenticated";
export const AUTH_Z_DIRECTIVE_NAME = "hasRole";
// The order of roles is important here as higher roles have bigger indexes
// and determine the order of hierarchy.
const ROLES = ['UNKNOWN', 'USER', 'PARTNER', 'ADMIN'];
/**
* Transform the schema resolvers for every field or object that has the auth directives.
* If the directive is present, run the requried checks, otherwise run the resolvers as normal.
*/
const getSchemaTransformer = (isAuthenticatedFn, getUserPermissionsFn) => {
const typeDirectiveArgumentMaps = {};
return {
authDirectiveTransformer: (schema) =>
mapSchema(schema, {
[MapperKind.TYPE]: type => {
// Save the authn info to the type fields
const authnDirective = getDirective(schema, type, AUTH_N_DIRECTIVE_NAME)?.[0];
if (authnDirective) {
typeDirectiveArgumentMaps[type.name] = authnDirective;
}
// Save the authz info to the type fields
const authzDirective = getDirective(schema, type, AUTH_Z_DIRECTIVE_NAME)?.[0];
if (authzDirective) {
typeDirectiveArgumentMaps[type.name] = authzDirective;
}
return undefined;
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
const authNDirective =
getDirective(schema, fieldConfig, AUTH_N_DIRECTIVE_NAME)?.[0] ?? typeDirectiveArgumentMaps[typeName];
const authZDirective =
getDirective(schema, fieldConfig, AUTH_Z_DIRECTIVE_NAME)?.[0] ?? typeDirectiveArgumentMaps[typeName];
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = function (source, args, context, info) {
const contextValue = context.headers[USER_HEADER];
// Check if the field requires authentication
if (authNDirective && !isAuthenticatedFn(contextValue)) {
throw new Error(`Not authenticated. The GraphQL context did not contain the ${USER_HEADER} value`);
}
// Get the required authorization role for the requested field
const requiredAuthZ = authZDirective?.requires;
if (requiredAuthZ) {
if (!contextValue) {
throw new Error(`Invalid role for authorization. The GraphQL context did not contain the ${USER_HEADER} value`);
}
const user = getUserPermissionsFn(contextValue);
if (!user.hasRole(requiredAuthZ)) {
throw new Error(`Not authorized. The provided role does not meet schema requirements`);
}
}
// Else, run the resolvers as normal
return resolve(source, args, context, info);
}
return fieldConfig;
}
})
}
};
/**
* This function is where you could do more advanced logic to parse a token, validate it,
* and check the user's permissions against the schema roles. For simplicity in the demo,
* we just accept the raw value from the header as the role.
*/
const getUserPermissions = roleDefinedInHeader => ({
hasRole: (roleRequiredInSchema) => {
const headerIndex = ROLES.indexOf(roleDefinedInHeader);
const schemaIndex = ROLES.indexOf(roleRequiredInSchema);
return schemaIndex >= 0 && headerIndex >= schemaIndex;
}
});
/**
* This function is called to validate the header value. In a real app this would parse some token
* and probably call and external service. For our simple demo we will just check the existence of the header.
*/
const isAuthenticated = (headerValue) => {
return !!headerValue
};
export const { authDirectiveTransformer } = getSchemaTransformer(isAuthenticated, getUserPermissions);