Who May Do What
In the last two chapters, we designed the shop API, a headless commerce API that merchants could use to build their own online shops. The API lets a merchant maintain a product catalog and stock, and lets shoppers browse products, manage carts, and place orders through a website or phone app.
We have decided what those requests look like and what the backend does with them. We have also organized the backend into layers, separating request handling from the shop API’s business rules and database access. So far, we have mostly assumed who is allowed to do each thing.
The requirements already contain restrictions. Only the merchant may change products. A shopper may read and change their own cart, and retrieve their own orders. The server must enforce these restrictions whenever it receives a request. A shopper must not be able to change a product’s price or read another shopper’s cart.
In this chapter, we make those restrictions part of the design. We will decide what information the server needs to allow or refuse an operation, and where it makes that decision.
Our examples use the GraphQL version of the shop API, but the concepts in this chapter also apply to REST APIs and to any other system that needs authentication and authorization.
Authorization vs authentication
The shop API’s design assumes that each request carries credentials identifying the caller. Verifying the caller’s identity is called authentication. How a person signs in and obtains those credentials is a separate problem. Here, for simplicity, we start from a server that has already verified who the caller is.
Once the server knows who sent a request, it still has to decide whether that caller may perform the requested action. That decision is authorization. Authentication answers “Who is this caller?” Authorization answers “May the caller do this?”
A request can pass the first check and fail the second. For example, the server can verify that a request came from a shopper with valid credentials and still refuse it, because that shopper is trying to read another shopper’s cart, or to create a product, which only the merchant may do.
Enforcing permissions on the server
The obvious way to keep shoppers from creating products is to never offer them the option. The storefront should not show an “Add product” button to a shopper. Only the merchant’s interface should show that control. That is good interface design, because a user should not be offered an action they are not allowed to perform.
But a caller does not have to use our frontend. They can write a program that sends the createProduct mutation directly to the API. If the API is public and documented, they can find that mutation and send a request from anywhere.
For example, suppose a mischievous shopper sends a request to create a product. The request has the correct arguments and valid credentials:
POST /graphql HTTP/1.1
Host: api.shop.example
Content-Type: application/json
Authorization: Bearer <valid-credentials>
{
"query": "mutation {
createProduct(
name: \"Red mug\"
description: \"A red ceramic mug.\"
priceCents: 1600
quantity: 10
) {
id
name
}
}"
}
Our frontend would never send this request for a shopper, but the server receives it anyway. The server must refuse it. Here is the response we give the shopper:
{
"data": {
"createProduct": null
},
"errors": [
{
"message": "Only a merchant may create products.",
"path": ["createProduct"],
"extensions": { "code": "FORBIDDEN" }
}
]
}
No product is created. FORBIDDEN is a code we define for the shop API, like INSUFFICIENT_STOCK. It is not a built-in GraphQL error code. The mutation result is nullable, so it can be null when the operation is refused. As we decided earlier in our design, we return HTTP status 200 and the client learns about the refusal from errors. In a REST API version, we would return a 403 Forbidden status instead.
So the server must not rely on the frontend to enforce permissions. It must check permission on every request it receives, including requests our frontend would never send.