GraphQL API Gateway Patterns
Authorization Facade

Authorization Facade Pattern for GraphQL API Gateways

This API Facade pattern adds a layer of authentication / authorization on top of one or more GraphQL APIs.

Problem

You have one or more internal GraphQL APIs that don't have the concept of authentication / authorization, instead they simply expose their business functionality in a very raw way. Building services like this can be quite powerful because it makes using the service quite flexible.

E.g. you could build a micro service for user management that exposes a GraphQL API. The API allows you to create and manage users as well as assigning roles to them. You don't want to expose this API directly to your clients because it would give them too much power. At the same time, we'd like to build a regular app for our users as well as an admin app to manage accounts. Both apps should be able to use the same API, but only the admin app should be able to create and manage users.

Solution

First, we need to build our API in a way that accepts a "principal" object as one of the arguments to each resolver. This principat object contains information about the current user or service that is calling the API. It allows our API to make decisions on wheter or not the current principal is allowed to perform a certain action, like reading a user object, or creating a new user.

Second, we need to build an authentication facade on top of our API to facilitate injecting the principal object. This facade is responsible for authenticating the user, which can be delegated to another service e.g. using OpenID Connect, and then inject the principal object into requests to the API.

What the API facade pattern allows us to do is to build a single GraphQL API that's flexible enough to serve multiple use cases without tieing it to a specific authentication / authorization mechanism.

Example

Let's build a simple API that allows us to manage users.

type User {
  id: ID!
  name: String!
  email: String!
  roles: [String!]!
}
 
type Query {
  users(principal: ID!): [User!]!
  user(id: ID!, principal!: ID!): User
}
 
type Mutation {
  createUser(name: String!, email: String!, principal: ID!): User!
  updateUser(id: ID!, name: String, email: String, principal: ID!): User!
  deleteUser(id: ID!, principal: ID!): Boolean!
  updateUserRoles(id: ID!, roles: [String!]!, principal: ID!): User!
}

The API is quite simple, it allows us to create, read, update and delete users as well as update their roles.

Next, we add the authentication facade pattern to our GraphQL API Gateway by adding two custom directives to the Gateway-exposed GraphQL Schema.

directive @requireAuth on OPERATION_DEFINITION
 
directive @injectClaim(
  name: CLAIM!
  on: String!
) on VARIABLE_DEFINITION
 
enum CLAIM {
  SUBJECT
  EMAIL
  NAME
}

When writing a mutation to create a new user, we can now use the directives to require authentication and inject the SUBJECT claim into the principal argument.

mutation CreateUser(input: CreateUserInput! $principal: ID! @injectClaim(name: SUBJECT)) @requireAuth {
    createUser(input: $input, principal: $principal) {
        id
        name
        email
        roles
    }
}
⚠️

This Pattern only works together with the Persisted Operations Pattern

The API Gateway will now ensure that the user is authenticated before calling this Operation. In addition, it will inject the SUBJECT claim from the authentication token into the principal argument.

It might not be immediately obvious, but if the user is allowed to define arbitrary queries and mutations, they could simply omit the requireAuth directive and define the principal argument themselves. When persisting the Operation, what the Persisted Operation Pattern does, this is prevented.

Claims are name value pairs of information about the user. One common format for claims is the JSON Web Token (JWT) format.

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Considerations

You can expand on this pattern by adding a layer of authorization to the API Gateway. This way, you can "outsource" the authorization logic to the API Gateway. This can be useful in a microservice architecture where you don't want to implement authorization in each service.

In more advanced and demanding environments, you could even build a custom authorization service or use an existing one like Open Policy Agent (OPA). This allows you to only use the API Gateway as a policy enforcement point (PEP) and delegate the authorization logic to a dedicated service.

Here's how a simple version of this could look like with Role Based Access Control (RBAC). Let's add another custom directive to our API Gateway-exposed GraphQL Schema.

directive @requireRole(
  roles: [ROLE!]!
) on OPERATION_DEFINITION
 
enum ROLE {
  ADMIN
  USER
}

We can now use this directive to require a certain role to be present in the principal object.

mutation CreateUser(input: CreateUserInput! $principal: ID! @injectClaim(name: SUBJECT)) @requireAuth @requireRole(roles: [ADMIN]) {
    createUser(input: $input, principal: $principal) {
        id
        name
        email
        roles
    }
}

To implement this pattern, we need to add a callback function to the API Gateway that gets called after the user has been authenticated. This callback function is responsible for validating all user claims and "storing" the principal object, e.g. in a cookie, JWT or session. On subsequent requests, the API Gateway loads the principal object, e.g. from the cookie, and injects all required information into the request.

Depending on the number of claims and the size of the principal object, you might run into limits of cookies, as they only allow you to store a limited amount of data. When using this pattern with cookies, you should only store the minimum amount of information in the cookie, encrypt the cookie and make it HttpOnly to prevent XSS attacks.

Advances use cases like impersonation

Finally, I'd like to show you how far you can take this pattern. We can even implement impersonation using this pattern.

Let's adjust our GraphQL Schema slightly to allow impersonation.

type User {
  id: ID!
  name: String!
  email: String!
  roles: [String!]!
}
 
type Query {
  users(principal: ID!): [User!]!
  user(id: ID!, principal!: ID!): User
}
 
type Mutation {
  createUser(name: String!, email: String!, principal: ID!, onBehalf: ID): User!
  updateUser(id: ID!, name: String, email: String, principal: ID!, onBehalf: ID): User!
  deleteUser(id: ID!, principal: ID!, onBehalf: ID): Boolean!
  updateUserRoles(id: ID!, roles: [String!]!, principal: ID!, onBehalf: ID): User!
}

With this change, we can now act on behalf of another user. Let's create a user as an admin on behalf of another user.

mutation CreateUser(input: CreateUserInput! $principal: ID! @injectClaim(name: SUBJECT)) @requireAuth @requireRole(roles: [ADMIN]) {
    createUser(input: $input, principal: $principal, onBehalf: "1234567890") {
        id
        name
        email
        roles
    }
}

Again, you can see how flexible and powerful this pattern can be. You can implement all kinds of advanced use cases with it without having to put too much logic into your services.

Real World Examples

At WunderGraph, we've implemented this pattern using the requireAuthentication (opens in a new tab) and fromClaim directive (opens in a new tab). For authentication, you can use any OpenID Connect provider, e.g. Auth0, Okta, Cognito, Keycloak, etc.

In addition, we've learned that some of our users want to add custom claims to the principal object, so we've enabled that as well (opens in a new tab).

All in all we found this pattern to be very useful and flexible. We're using it successfully in our own cloud offering which uses the open source WunderGraph Gateway under the hood.