Skip to main content

GraphQL API

What you'll learn
  • prevent unauthorized users from performing sensitive GraphQL API queries and mutations
info

Learn more about the Webiny Security Framework in the Security Framework key topics section.

Overview#

info

The code that we will cover in this tutorial can also be found in our GitHub examples repository.

At the moment, the carManufacturers queries and mutations, that we added to our main GraphQL API in the API Package tutorial, are publicly exposed.

For example, if were to perform a simple listCarManufacturers GraphQL query, we would receive a list of all car manufacturers:

Exposed GraphQL Queries

Even more important, anybody can create new car manufacturers, with the createCarManufacturer GraphQL mutation:

Exposed GraphQL Mutations

In most cases, this is not the desirable behaviour, and you'll certainly want to have control over who can perform these GraphQL operations, and who cannot.

Securing GraphQL API Resolvers#

In order to secure our GraphQL API, we have to revisit the GraphQL resolver functions that were generated for us via the GraphQL API package scaffold.

info

Learn more about GraphQL resolver functions in the official GraphQL documentation article.

All of the GraphQL resolver functions are located in the packages/car-manufacturers/api/src/resolvers folder:

packages/car-manufacturers/api/
.
โ”œโ”€โ”€ __tests__
โ”œโ”€โ”€ dist
โ”œโ”€โ”€ node_modules
โ””โ”€โ”€ src
โ””โ”€โ”€ resolvers
โ”œโ”€โ”€ createCarManufacturer.ts
โ”œโ”€โ”€ deleteCarManufacturer.ts
โ”œโ”€โ”€ getCarManufacturer.ts
โ”œโ”€โ”€ index.ts
โ”œโ”€โ”€ install.ts
โ”œโ”€โ”€ isInstalled.ts
โ”œโ”€โ”€ listCarManufacturers.ts
โ”œโ”€โ”€ uninstall.ts
โ””โ”€โ”€ updateCarManufacturer.ts

For purposes of this tutorial, we can open the simplest resolver, which is the getCarManufacturer.ts, and add the following lines of code:

packages/car-manufacturers/api/src/resolvers/getCarManufacturer.ts
import { Response, NotFoundResponse } from "@webiny/handler-graphql";
import { NotAuthorizedResponse } from "@webiny/api-security";
// We use this when specifying the return types of the getPermission function call (below).
import { FullAccessPermission } from "@webiny/api-security/types";
import { utils } from "../utils";
import {
ApplicationContext,
GetCarManufacturerArgs,
ResolverResponse,
CarManufacturer,
// Creating types for security permissions makes our code less error-prone and more readable.
CarManufacturersPermission
} from "../types";
const getCarManufacturer = async (
_,
args: GetCarManufacturerArgs,
context: ApplicationContext
): Promise<ResolverResponse<CarManufacturer>> => {
// First, check if the current identity can perform the "getCarManufacturer" query,
// within the detected locale. An error will be thrown if access is not allowed.
const hasLocaleAccess = await context.i18nContent.hasI18NContentPermission();
if (!hasLocaleAccess) {
return new NotAuthorizedResponse();
}
// Next, check if the current identity possesses the "car-manufacturers" permission.
// Note that, if the identity has full access, "FullAccessPermission" permission
// will be returned instead, which is equal to: { name: "*"}.
const permission = await context.security.getPermission<
CarManufacturersPermission | FullAccessPermission
>("car-manufacturers");
if (!permission) {
return new NotAuthorizedResponse();
}
// Note that the received permission object can also be `{ name: "*" }`. If so, that
// means we are dealing with the super admin, who has unlimited access.
let hasAccess = permission.name === "*";
if (!hasAccess) {
// If not super admin, let's check if we have the "r" in the `rwd` property.
hasAccess =
permission.name === "car-manufacturers" &&
permission.rwd &&
permission.rwd.includes("r");
}
// Finally, if current identity doesn't have access, we immediately exit.
if (!hasAccess) {
return new NotAuthorizedResponse();
}
const { db } = context;
const { id } = args;
const primaryKey = utils.createPk(context, id);
const response = await db.read<CarManufacturer>({
...utils.db(context),
query: {
PK: primaryKey,
SK: id
},
limit: 1
});
const [items] = response;
const [item] = items;
if (!item) {
return new NotFoundResponse(`CarManufacturer with id "${id}" not found.`);
}
return new Response(item);
};
export default getCarManufacturer;
tip

If you're curious about the CarManufacturersPermission type, check its definition in packages/car-manufacturers/api/src/types.ts:173.

info

In order to actually compile the code changes we're about to make and see them in our GraphQL API, we need to run the following Webiny CLI command:

yarn webiny watch api/code/graphql --env dev

To learn more, check out the Use the Watch Command guide.

Now, to see this new piece of authorization logic in action, we can use a GraphQL client, for example the GraphQL Playground, point it to our GraphQL API's URL, and try executing the following query:

{
carManufacturers {
# Replace the `id` with the value that exists in your system.
getCarManufacturer (id: "6082ad846c073d0009ff0067") {
data {
id
title
isNice
}
error {
message
code
data
}
}
}
}
Misplaced GraphQL API URL?

Running the yarn webiny info command in your Webiny project folder will give you all of the relevant project URLs, including the URL of your GraphQL API.

Without including the appropriate Authorization request header, we should receive the following error response:

GraphQL Authorization Error

If you received the SECURITY_NOT_AUTHORIZED error, that means authorization was successful.

On the other hand, if a valid Authorization request header is included, or in other words, the current identity actually has access to the Car Manufacturers module, the data should correctly be returned.

To manually test that, we can simply create a new user via the Webiny Security application, and either place it into the default Full Access security group, or even better, into the newly created Car Manufacturers security group. Which is what the following screenshot is showing:

Create a Test User

Once we've created the user, we can log in into the Webiny Admin Area with it (using its username and password), and execute the same GraphQL query, this time using the built-in API Playground. We will use this client simply because of the fact that, upon issuing GraphQL operations, it will automatically attach the correct Authorization request header for us. In other words, it will perform GraphQL operations as the currently logged in user.

info

Learn more about the API Playground GraphQL client in the API Playground guide.

So, as we can see in the following screenshot, the GraphQL query was successful, as we've successfully received the car manufacturer data in the response:

GraphQL Authorization Success

This means that the user we're currently logged in with has the appropriate security permissions, and that the newly added authorization code works as expected.

Final Notes#

Before we wrap this up, note that this is just a single resolver we secured, and that you should implement the same logic into others as well.

Finally, in case you start seeing yourself copying some of the authorization related code, it's certainly recommended that you extract it into one or more separate utility functions. This way we're not repeating our selves (DRY) and our code will be more maintainable and less error-prone.

FAQ#

I'm not building a multi-locale Admin Area module.#

If you're not building a multi-locale module, feel free to skip the await context.i18nContent.hasI18NContentPermission(); checks in your GraphQL resolvers and simply start with your own authorization checks.

But of course, be aware that if your project ends up being multi-locale, then users will be able to execute your GraphQL resolvers in every created locale (unless they are prevented by your custom authorization logic).

Last updated on by Pavel Denisjuk