UNPKG

@envelop/generic-auth

Version:

This plugin allows you to implement custom authentication flow by providing a custom user resolver based on the original HTTP request. The resolved user is injected into the GraphQL execution `context`, and you can use it in your resolvers to fetch the cu

235 lines (233 loc) • 10.1 kB
const require_utils = require('./utils.cjs'); let graphql = require("graphql"); let _envelop_extended_validation = require("@envelop/extended-validation"); let _graphql_tools_executor = require("@graphql-tools/executor"); let _graphql_tools_utils = require("@graphql-tools/utils"); let _whatwg_node_promise_helpers = require("@whatwg-node/promise-helpers"); //#region src/index.ts const DIRECTIVE_SDL = ` directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE `; const SKIP_AUTH_DIRECTIVE_SDL = ` directive @skipAuth on FIELD_DEFINITION | OBJECT | INTERFACE `; const REQUIRES_SCOPES_DIRECTIVE_SDL = ` directive @requiresScopes(scopes: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE `; const POLICY_DIRECTIVE_SDL = ` directive @policy(policies: [String!]!) on FIELD_DEFINITION | OBJECT | INTERFACE `; function createUnauthenticatedError(params) { return (0, _graphql_tools_utils.createGraphQLError)(params?.message ?? "Unauthorized field or type", { nodes: params?.fieldNode ? [params.fieldNode] : void 0, path: params?.path, extensions: { code: "UNAUTHORIZED_FIELD_OR_TYPE", http: { status: params?.statusCode ?? 401 } } }); } function defaultProtectAllValidateFn(params) { if (params.user == null && !params.fieldAuthArgs && !params.typeAuthArgs) return createUnauthenticatedError({ fieldNode: params.fieldNode, path: params.path }); return validateScopesAndPolicies(params); } function areRolesValid(requiredRoles, userRoles) { for (const roles of requiredRoles) if (roles.every((role) => userRoles.includes(role))) return true; return false; } function validateRoles(params, requiredRoles, userRoles) { if (!areRolesValid(requiredRoles, userRoles)) return createUnauthenticatedError({ fieldNode: params.fieldNode, path: params.path }); } function validateScopesAndPolicies(params) { if (params.typeScopes) { const error = validateRoles(params, params.typeScopes, params.userScopes); if (error) return error; } if (params.typePolicies?.length) { const error = validateRoles(params, params.typePolicies, params.userPolicies); if (error) return error; } if (params.fieldScopes?.length) { const error = validateRoles(params, params.fieldScopes, params.userScopes); if (error) return error; } if (params.fieldPolicies?.length) { const error = validateRoles(params, params.fieldPolicies, params.userPolicies); if (error) return error; } } function defaultProtectSingleValidateFn(params) { if (params.user == null && (params.fieldAuthArgs || params.typeAuthArgs)) return createUnauthenticatedError({ fieldNode: params.fieldNode, path: params.path }); return validateScopesAndPolicies(params); } function defaultExtractScopes(user) { if (user != null && typeof user === "object" && "scope" in user) { if (typeof user.scope === "string") return user.scope.split(" "); if (Array.isArray(user.scope)) return user.scope; } return []; } const useGenericAuth = (options) => { const contextFieldName = options.contextFieldName || "currentUser"; if (options.mode === "protect-all" || options.mode === "protect-granular") { const authDirectiveName = options.authDirectiveName ?? (options.mode === "protect-all" ? "skipAuth" : "authenticated"); const requiresScopesDirectiveName = options.scopesDirectiveName ?? "requiresScopes"; const policyDirectiveName = options.policyDirectiveName ?? "policy"; const validateUser = options.validateUser ?? (options.mode === "protect-all" ? defaultProtectAllValidateFn : defaultProtectSingleValidateFn); const extractScopes = options.extractScopes ?? defaultExtractScopes; const rejectUnauthenticated = "rejectUnauthenticated" in options ? options.rejectUnauthenticated !== false : true; const policiesByContext = /* @__PURE__ */ new WeakMap(); return { onPluginInit({ addPlugin }) { addPlugin((0, _envelop_extended_validation.useExtendedValidation)({ rejectOnErrors: rejectUnauthenticated, onDocument: require_utils.removeEmptyOrUnusedNodes, rules: [function AuthorizationExtendedValidationRule(context, args) { const user = args.contextValue[contextFieldName]; const schema = context.getSchema(); const operationAST = (0, graphql.getOperationAST)(args.document, args.operationName); const variableDefinitions = operationAST?.variableDefinitions; let variableValues; if (variableDefinitions?.length) { const { coerced } = (0, _graphql_tools_executor.getVariableValues)(schema, variableDefinitions, args.variableValues || {}); variableValues = coerced; } else variableValues = args.variableValues; const operationType = operationAST?.operation ?? "query"; const fragmentPaths = /* @__PURE__ */ new Map(); function getResolvePath(path, currType) { const resolvePath = []; let curr = args.document; for (const pathItem of path) { curr = curr[pathItem]; if (curr?.kind === "Field") { const fieldName = curr.name.value; const responseKey = curr.alias?.value ?? fieldName; let field; if ((0, graphql.isObjectType)(currType)) field = currType.getFields()[fieldName]; else if ((0, graphql.isAbstractType)(currType)) for (const possibleType of schema.getPossibleTypes(currType)) { field = possibleType.getFields()[fieldName]; if (field) break; } if ((0, graphql.isListType)(field?.type)) resolvePath.push("@"); resolvePath.push(responseKey); if (field?.type) currType = (0, graphql.getNamedType)(field.type); } else if (curr?.kind === "FragmentDefinition") { currType = schema.getType(curr.typeCondition.name.value); const fragmentPath = fragmentPaths.get(curr.name.value); if (fragmentPath) resolvePath.push(...fragmentPath); } } return resolvePath; } const handleField = ({ node: fieldNode, path }, parentType) => { const field = parentType.getFields()[fieldNode.name.value]; if (field == null) return; const typeDirectives = parentType && (0, _graphql_tools_utils.getDirectiveExtensions)(parentType, schema); const typeAuthArgs = typeDirectives[authDirectiveName]?.[0]; const typeScopes = typeDirectives[requiresScopesDirectiveName]?.[0]?.["scopes"]; const typePolicies = typeDirectives[policyDirectiveName]?.[0]?.["policies"]; const fieldDirectives = (0, _graphql_tools_utils.getDirectiveExtensions)(field, schema); const fieldAuthArgs = fieldDirectives[authDirectiveName]?.[0]; const fieldScopes = fieldDirectives[requiresScopesDirectiveName]?.[0]?.["scopes"]; const fieldPolicies = fieldDirectives[policyDirectiveName]?.[0]?.["policies"]; const userScopes = extractScopes(user); const userPolicies = policiesByContext.get(args.contextValue) ?? []; return validateUser({ user, fieldNode, parentType, typeScopes, typePolicies, typeAuthArgs, typeDirectives, executionArgs: args, field, fieldDirectives, fieldAuthArgs, fieldScopes, fieldPolicies, userScopes, path: getResolvePath(path, (0, _graphql_tools_utils.getDefinedRootType)(schema, operationType)), userPolicies }); }; return { FragmentSpread(node, _key, _parent, path) { const fragmentName = node.name.value; const fragment = context.getFragment(fragmentName); if (fragment) { const resolvePath = getResolvePath(path, schema.getType(fragment.typeCondition.name.value)); fragmentPaths.set(fragmentName, resolvePath); } }, Field(node, key, parent, path, ancestors) { if (variableValues && !(0, _graphql_tools_utils.shouldIncludeNode)(variableValues, node)) return; const fieldType = (0, graphql.getNamedType)(context.getParentType()); if ((0, graphql.isIntrospectionType)(fieldType)) return node; if ((0, graphql.isUnionType)(fieldType)) for (const objectType of fieldType.getTypes()) { const error = handleField({ node, key, parent, path, ancestors }, objectType); if (error) { context.reportError(error); return null; } } else if ((0, graphql.isObjectType)(fieldType) || (0, graphql.isInterfaceType)(fieldType)) { const error = handleField({ node, key, parent, path, ancestors }, fieldType); if (error) { context.reportError(error); return null; } } } }; }] })); }, onContextBuilding({ context, extendContext }) { return (0, _whatwg_node_promise_helpers.handleMaybePromise)(() => options.resolveUserFn(context), (user) => { if (context[contextFieldName] !== user) extendContext({ [contextFieldName]: user }); if (options.extractPolicies) return (0, _whatwg_node_promise_helpers.handleMaybePromise)(() => user && options.extractPolicies?.(user, context), (policies) => { if (policies?.length) policiesByContext.set(context, policies); }); }); } }; } if (options.mode === "resolve-only") return { onContextBuilding({ context, extendContext }) { return (0, _whatwg_node_promise_helpers.handleMaybePromise)(() => options.resolveUserFn(context), (user) => { extendContext({ [contextFieldName]: user }); }); } }; return {}; }; //#endregion exports.DIRECTIVE_SDL = DIRECTIVE_SDL; exports.POLICY_DIRECTIVE_SDL = POLICY_DIRECTIVE_SDL; exports.REQUIRES_SCOPES_DIRECTIVE_SDL = REQUIRES_SCOPES_DIRECTIVE_SDL; exports.SKIP_AUTH_DIRECTIVE_SDL = SKIP_AUTH_DIRECTIVE_SDL; exports.createUnauthenticatedError = createUnauthenticatedError; exports.defaultExtractScopes = defaultExtractScopes; exports.defaultProtectAllValidateFn = defaultProtectAllValidateFn; exports.defaultProtectSingleValidateFn = defaultProtectSingleValidateFn; exports.useGenericAuth = useGenericAuth;