How to add Wrap resolver in NestJS and GraphQL to check if email from header is equal to the email in query

967 views Asked by At

I'm using cognito authentication, I create a middlware

const { email } = payload;
req.headers['user-email'] = email as string;

I want to write this kind of function

 public async httpCheck(query: any, args: any, context: any, 
resolveInfo: any) {
console.log('authhealth');
console.log("context "+ context.userEmail);
console.log("query : "+ query.userEmail);
(context.userEmail === query.userEmail ) ? console.log("authorized successfully") : console.log("authorization failed"); 
return 'OK';

}

This is my file structure, I want to write wrap resolver

enter image description here

1

There are 1 answers

0
THX1138 On

From your example, it looks like you are wanting to reject the whole request if the email from the request header does not match an email being provided as an argument to a field in the GraphQL query.

So given the following query:

query MyQuery($userEmail:String!) {
  userByEmail(email: $userEmail) {
    id
    email
    familyName
    givenName
  }
}

If you want to check that the header email equals the email argument of userByEmail BEFORE Postgraphile executes the operation, you need to use a Postgraphile Server Plugin which adds a dynamic validation rule that implements the check:

import type { PostGraphilePlugin } from "postgraphile";
import type { ValidationRule } from "graphql";
import { GraphQLError } from "graphql";
import type { IncomingMessage } from "http";
import type { Plugin } from "graphile-build";

// Defines a graphile plugin that uses a field argument build hook to add
// metadata as an extension to the "email" argument of the "userByEmail" field
const AddEmailMatchPlugin: Plugin = (builder) => {
  builder.hook(
    "GraphQLObjectType:fields:field:args",
    (args, build, context) => {
      // access whatever data you need from the field context. The scope contains
      // basically any information you might desire including the database metadata
      // e.g table name, primary key.
      const {
        scope: { fieldName, isRootQuery },
      } = context;

      if (!isRootQuery && fieldName !== "userByEmail") {
        return args;
      }

      if (args.email) {
        return {
          ...args,
          email: {
            ...args.email,
            // add an extensions object to the email argument
            // this will be accessible from the finalized GraphQLSchema object
            extensions: {
              // include any existing extension data
              ...args.email.extensions,
              // this can be whatetever you want, but it's best to create
              // an object using a consistent key for any
              // GraphQL fields/types/args that you modify
              myApp: {
                matchToUserEmail: true,
              },
            },
          },
        };
      }

      return args;
    }
  );
};

// define the server plugin
const matchRequestorEmailWithEmailArgPlugin: PostGraphilePlugin = {
  // this hook enables the addition of dynamic validation rules
  // where we can access the underlying http request
  "postgraphile:validationRules": (
    rules,
    context: { req: IncomingMessage; variables?: Record<string, unknown> }
  ) => {
    const {
      variables,
      // get your custom user context/jwt/headers from the request object
      // this example assumes you've done this in some upstream middleware
      req: { reqUser },
    } = context;

    if (!reqUser) {
      throw Error("No user found!");
    }

    const { email, role } = reqUser;

    const vr: ValidationRule = (validationContext) => {
      return {
        Argument: {
          // this fires when an argument node has been found in query AST
          enter(node, key) {
            if (typeof key === "number") {
              // get the schema definition of the argument
              const argDef = validationContext.getFieldDef()?.args[key];
              if (argDef?.extensions?.myApp?.matchToUserEmail) {
                // restrict check to a custom role
                if (role === "standard") {
                  const nodeValueKind = node.value.kind;
                  let emailsMatch = false;

                  // basic case
                  if (nodeValueKind === "StringValue") {
                    if (node.value.value === email) {
                      emailsMatch = true;
                    }
                  }
                  // must account for the value being provided by a variable
                  else if (nodeValueKind === "Variable") {
                    const varName = node.value.name.value;
                    if (variables && variables[varName] === email) {
                      emailsMatch = true;
                    }
                  }

                  if (!emailsMatch) {
                    validationContext.reportError(
                      new GraphQLError(
                        `Field "${
                          validationContext.getFieldDef()?.name
                        }" argument "${
                          argDef.name
                        }" must match your user email.`,
                        node
                      )
                    );
                  }
                }
              }
            }
          },
        },
      };
    };
    return [...rules, vr];
  },

  // This hook appends the AddEmailMatchPlugin graphile plugin that
  // this server plugin depends on for its custom extension.
  "postgraphile:options": (options) => {
    return {
      ...options,
      appendPlugins: [...(options.appendPlugins || []), AddEmailMatchPlugin],
    };
  },
};

export default matchRequestorEmailWithEmailArgPlugin;

Then you need to register the server plugin in the Postgraphile middleware options:

const pluginHook = makePluginHook([MatchRequestorEmailWithEmailArgPlugin]);

const postGraphileMiddleware = postgraphile(databaseUrl, "my_schema", {
  pluginHook,
  // ...
});

If you just want to reject the userByEmail field in the query and don't care about rejecting before any resolution of any other parts of the request occur, you can use the makeWrapResolversPlugin to wrap the resolver and do the check there.