Is Securing GraphQL APIs Harder?

Here at StepZen, we're often asked about securing GraphQL APIs.

"We've heard GraphQL is a security risk. Is it? Is securing GraphQL harder than securing REST APIs?"

Of course, security isn't a boolean, it's a gradient. The question isn't a matter of "secure vs insecure"; it's "more secure vs less secure."

If you only consider that the main method of interacting with a GraphQL API is an HTTP request, the answer is no, it's not harder. There are time-proven ways of handling security for HTTP.

But when you look at GraphQL one step beyond the network access control perspective, it can be harder to make it more secure. You have a single endpoint that may expose a broader capability than a REST API endpoint – especially if you federate multiple services into a single GraphQL endpoint. From that perspective, granting access to the endpoint may generate more risk than granting access to a single REST API endpoint. Therefore we need some ability to control access to the GraphQL schema that lies beyond the endpoint.

In this blog, we’ll take a look at StepZen Field Policies, which allow you to control access to your GraphQL API beyond simply approving or blocking the HTTP request. Field Policies can be used on APIs created in StepZen or applied to any GraphQL API, regardless of implementation technology.

GraphQL API Entry Points

Recall that a GraphQL API has, at the top level, root types under which all the fields, or entry points, for the API are defined. Typically those root types are Query and Mutation. Let's look at an example where we have the two root types, Query and Mutation. Each, in turn, has two fields, or entry points, defined.

type Query {
  users(email: String): [User]
  orders(userId: ID): [Order]
}
 
type Mutation {
  createUser(username: String!, email: String!, companyName: String): User
  addItemToOrder(
    orderId: ID!
    productId: ID!
    quantity: Int!
    price: Float!
  ): Order
}

The Query root type has the users and orders fields defined. And the Mutation root type has createUser and addItemToOrder fields defined.

If the access control for this API relies solely on HTTP endpoint access, anyone with access to the endpoint can execute an operation using any of the four entry point fields. In most real-world situations you would either need to lock down the API too much, limiting the value of GraphQL, or allow access to more callers while implementing security further down the stack, leading to much greater risk. We need a solution that allows us to restrict access beyond simply restricting access to the endpoint. We need a solution that recognizes the capabilities of GraphQL and can control access to the fields in the GraphQL request.

Access Control with StepZen Field Policies

StepZen Field Policies let you define rules under which those top-level fields, or entry points, may be accessed when a request is executed. This means you can control access to your GraphQL API beyond simply approving or blocking the HTTP request. Like any other feature in StepZen, field policies are configuration, not code. Rather than live in the defined schema for a GraphQL API, they live in the configuration file that specifies the deployment of an API. Let's look at the anatomy of a field policy. You define field policies in YAML, and you put them in the config.yaml for your StepZen endpoint. Here is a generic example:


access:
 policies:
   - type: Query
     policyDefault:
       condition: PREDICATE
     rules:
       - name: "name of rule"
         condition: PREDICATE
         fields: ["entry1"]
       - name: "name of rule"
         condition: PREDICATE
         fields: ["entry2", "entry3"]
   - type: Mutation
     policyDefault:
       condition: PREDICATE
     rules:
       - name: "name of rule"
         condition: PREDICATE
         fields: ["entry4"]

There are two policy types supported, Query and Mutation. For each type, there is a policyDefault, which allows you to specify the condition to apply when a field is not specified in any rule. The default condition for policyDefault is false, meaning that fields are not accessible. To make any field not specified in a rule accessible through any request, you can set the condition to true.

Note! Setting condition to true would allow any request to access that field.

For each policy type you can specify multiple rules. A rule is made up of a name, condition, and set of fields to which it applies. The name is useful for administration purposes and for keeping track of why a rule exists. It is visible only in the config.yaml file. The condition evaluates to a boolean indicating whether access to the field or fields listed in the fields property is allowed.

That's all there is to it. The only restriction is that a field must be listed in only one rule. When dealing with security it is important to avoid any ambiguity, and allowing fields in multiple rules creates ambiguity. In practice, this restriction is invaluable in preventing unanticipated access to the fields you wish to protect.

In the next section, we'll look at a real-world example of applying field rules to a mock endpoint, using JSON Web Token (JWT) attributes in the conditions.

Using Field Policies with JWT

JSON Web Tokens (JWT) are industry-standard credentials for representing claims, typically about a user. To learn more about JWT in general, visit JWT.io. By combining JWT claims, or attributes, with StepZen field policies we can create an Attribute-Based Access Control (ABAC) framework for our GraphQL API. (If a user's role is an attribute of the JWT, you could just as easily use field policies to build a Role-Based Access Control (RBAC) framework for your GraphQL API). However, before we can use JWT in our policy rules we need to inform our StepZen configuration on how to validate the tokens.

As with Field Policies, this configuration is also done in the configuration file for the deployment of an endpoint. You specify the identity information required to verify the JWT – either by setting the JWKS URL in the case of OpenID-based identity, or by specifying the algorithm and keys used to sign the JWT. If you're not familiar with JWT this can sound a little complicated, but it's straightforward once you have a bit of background. We'll walk through a very simple example below. For more information on configuring StepZen to validate tokens, please see the online documentation: JWT-based access control.

Using JWT Attributes in Rules

Once StepZen is configured to validate JWT, you can then make GraphQL requests to your StepZen endpoint using a JWT in the Authorization header. StepZen validates the token using the identity information you provide in the configuration, and the token claims will be available to refer to in the Policy rules. Let's take a look at a few examples to see how we might configure the rules to use JWT information.

First, let's look at a policy that allows a user to make a request for any Query field if they provide a valid JWT in the authorization header:

access:
 policies:
   - type: Query
     policyDefault:
       condition: ?$jwt

This policy says that the default condition for any field of Query is the existence (? operator) of a valid JWT ($jwt variable). If there are any Mutations defined in your API, access to those would require the StepZen default access control method, API key (see Secure Your Schema and Endpoint for more information on default access control).

Example: Users with different levels of access control

Now imagine a GraphQL API for a blog website where you want anyone to be able to view posts, but only logged-in users to be able to comment and view comments. Your Policy definition could look like the following, in part:

access:
 policies:
   - type: Query
     rules:
       - name: "no access control"
         condition: true
         fields: ["posts"]
       - name: "logged in queries"
         condition: ?$jwt
         fields: ["comments"]
   - type: Mutation
     rules:
       - name: "logged in mutations"
         condition: ?$jwt
         fields: ["createComment"]

Let's consider a scenario where we have users with different levels of access control. We'll have editors, administrators, and authors. In this situation, we'll only look at the mutations. Editors can edit and publish posts, administrators can only edit posts, and authors can create and edit posts. Our authorization system assigns roles in the JWT roles claim (RFC7643, Section 4.1.2)[https://www.iana.org/go/rfc7643]. Our access policies would look like the following, in part:

access:
 policies:
   - type: Mutation
     rules:
       - name: "create posts"
         condition: $jwt.roles:String has "author" 
         fields: ["createPost"]
       - name: "edit posts"
         condition: $jwt.roles:String has "editor" || $jwt.roles:String has "administrator" || $jwt.roles:String has "author"
         fields: ["editPost"]
       - name: "publish posts"
         condition: $jwt.roles:String has "editor"
         fields: ["publishPost"]

This example shows a few key points about Field Policies and their conditions.

Firstly, a field may only appear in one rule in a policy. This is to prevent ambiguous access control decisions. Often people start thinking about conditions, and the fields to which those conditions have access. This is most common when thinking about this from a role-based access control perspective. It's easier to define rules when you flip your perspective to start with the fields and think about which conditions have access to those fields. In the example above, look at the "edit posts" rule. The field is editPost, and the conditions that have access are where the roles claim contains editor, administrator, or author. Thinking about rules this way makes it easier to avoid ambiguity when defining your Field Policies.

Secondly, the condition is built using a simple expression language. This language is fairly strict since we're dealing with access control. Part of that strictness means we specify the type of the variables we use in the conditions, e.g. $jwt.roles:String says the items in the list of roles are Strings, so when we use the 'has' operator we can use a string on the right-hand side. For more details on the condition, or predicate, language, refer to the Predicates in the StepZen docs.

A Complete Example You Can Run

If you would like to try a sample before testing with your API, we've provided one in our snippets repository. In the protection directory of that repository you will find a simpleABACSample subdirectory (direct link). Follow the instructions in the README to see how you can use Field Policies to protect any GraphQL API quickly and easily!

Summary

All APIs, including GraphQL, present an enticing target for cyber criminals. Exploiting the API can enable access to sensitive data or protected functionality within an application. GraphQL APIs share many of the same risks and attack vectors as REST APIs, and maybe even greater risk given that GraphQL delivers a single endpoint that, by design, exposes broader capability than a REST endpoint. But they also share the main method of interacting with them — an HTTP request, and there are proven ways of handling security for HTTP.

StepZen supports two methods to control access to your schemas and GraphQL endpoints.

API Keys control access to the entire endpoint. API keys are a typical access control method for APIs, and the default access control mechanism.

Field Policies provide fine-grained access control to your GraphQL API using a model similar to attribute-based access control. Field Policies can leverage industry-standard JSON Web Tokens, and provide specific controls for GraphQL APIs. Using a configuration-based mechanism, you can set access control rules for fields in the entry points of your GraphQL API.

Have questions about API security? Want to brainstorm a scenario? We’d love to hear from you over on our Discord Community or you can schedule time.