@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
JavaScript
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;