Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


44. Add Verify User Permissions Check

Objective: Create a function that will verify that a user can take the particular action that they are attempting against the the GraphCMS server.

We will create a function now that will verify a user's permissions when they try and perform 1 of 4 possible protected mutations updateRecipe, deleteAsset, deleteRecipe, and deleteUserLike. In order to verify that they are allowed to perform the mutation, we will first run a query on the recipe or userLike item that is being modified and verify that the owner matches the current user id. If they don't match then the function will throw an error and the user will be blocked from making the request.

Add the following verifyUserPermissions function to the verify.ts. file. The final file will end up looking like this:

utils/verify.ts

import * as _ from 'lodash';
import { print } from 'graphql';
import { getUserObject } from './getUserObject';
import auth0 from './auth0';
import { recipeGraphQL } from '../graphql/queries/recipe';
import { userLikeGraphQL } from '../graphql/queries/userLike';
import { recipesGraphQL } from '../graphql/queries/recipes';
import { graphQLClient } from '../pages/api/graphql';
export const verifyNotABannedMutation = async (req, res) => {
const isBannedMutation = req.body.query.match(
/deleteMany|updateMany|publishMany/g,
);
if (!_.isNil(isBannedMutation)) {
throw new Error('Invalid Mutation Requested');
}
};
export const verifyUserMutation = async (req, res) => {
const requestedUserId = getUserObject(req.body.variables);
if (!_.isNil(requestedUserId)) {
const { user } = await auth0.getSession(req);
const actualUserId: string = _.get(user, 'sub');
if (actualUserId !== requestedUserId) {
throw new Error('Invalid User Requested');
}
}
};
export const verifyUserPermissions = async (req, res) => {
const { variables } = req.body;
const mutationToMatch = [
{
match: /deleteRecipe/g,
queryToCheck: print(recipeGraphQL),
vars: variables,
path: 'recipe.owner',
},
{
match: /deleteUserLike/g,
queryToCheck: print(userLikeGraphQL),
vars: variables,
path: 'userLike.user',
},
{
match: /updateRecipe/g,
queryToCheck: print(recipeGraphQL),
vars: variables,
path: 'recipe.owner',
},
];
const doAnyVerificationsFail = await Promise.all(
mutationToMatch.map(async ({ match, queryToCheck, path, vars }) => {
const hasMatch = req.body.query.match(match);
if (!_.isNil(hasMatch)) {
const { user } = await auth0.getSession(req);
const actualUserId: string = _.get(user, 'sub');
const result = await graphQLClient.request(queryToCheck, vars);
const owner = _.get(result, path);
if (owner !== actualUserId) {
return true;
}
return false;
}
}),
);
if (doAnyVerificationsFail.some((b) => !!b)) {
throw new Error('You are not authorized to make that change.');
}
};

We have an array of objects called mutationsToMatch. In the event that the graphQL request contains a key phrase defined in the match field, we will run the query defined in the queryToCheck field with the variables given in the vars field. Once we run that query, we will find owner by searching in the response at the path defined by the path variable. Finally, once we have that owner string we can compare it against the current user id to ensure that they match. We can do this, because in our app we never allow one user to modify a different user's userLikes or recipes. This logic would need to be tweaked if we ever wanted to add a collaboration or admin feature, but this assumption suits us well for the time being.

Finally, update the graphql api function to call verifyUserPermissions along with the other checks.

pages/api/graphql.ts

import getConfig from 'next/config';
import { GraphQLClient } from 'graphql-request';
import {
verifyNotABannedMutation,
verifyUserMutation,
verifyUserPermissions,
} from '../../utils/verify';
const { serverRuntimeConfig } = getConfig();
const {
BRANCH,
GRAPHCMSURL,
GRAPHCMSPROJECTID,
GRAPHCMS_TOKEN,
} = serverRuntimeConfig.graphcms;
const graphqlEndpoint = `${GRAPHCMSURL}/${GRAPHCMSPROJECTID}/${BRANCH}`;
export const graphQLClient = new GraphQLClient(graphqlEndpoint, {
headers: {
authorization: `Bearer ${GRAPHCMS_TOKEN}`,
},
});
async function proxyGraphql(req, res) {
try {
await verifyNotABannedMutation(req, res);
await verifyUserMutation(req, res);
await verifyUserPermissions(req, res);
const { variables, query } = req.body;
const data = await graphQLClient.rawRequest(query, variables);
res.json(data);
} catch (e) {
res.json({ data: {}, errors: [{ message: e.message }] });
}
}
export default proxyGraphql;