Managing Introspection

StepZen provides GraphQL introspection for all endpoints. For public or externally facing APIs where you have developers working to use your API, this is a great tool and service. However, if you have an API that's a bit more closed, you will probably want to disable introspection in production. It is commonly listed as a best practice and is certainly best to do when you want your API to be closed or to tamp down on information leakage — you'll find other ways of masking your APIs such as stripping comments, renaming types, etc. to hide away details of your endpoint.

Specifically, GraphQL introspection allows extraction of the schema from a GraphQL endpoint. This includes the types, fields, and descriptions. This, of course, is particularly important in code-first GraphQL implementations where the schema is only defined by the code. There are a number of tools,such as GraphiQL, that make extensive use of this information to provide structured views of your schema, auto-completion, and descriptions. It's a powerful reflection tool. Let's start with a brief look at what can be done with introspection. Take the following schema.

""" Post is public """
type Post {
  postmessage : String!
}
""" News is public """
type News {
  newsMessage: String!
}
""" User should not be public """
type User {
  user: String!
}
""" Query """
type Query {
   """ Get Posts """
   posts : [Post!]
   """ Get News """
   news : [News!]
   """ Get Users """
   users : [User!]
}

GraphQL introspection is performed by examining the built-in Query type __schema field. Here's a simplified query on __schema:

{
  __schema {
    directives { name }
    queryType {
      fields {  name  description }
    }
    types { name description }
  }
}

and even trimmed, you can see the depth of the information available:

{
  "data": {
    "__schema": {
      "directives": [
        {
          "name": "include"
           "description": "Directs the executor to include this field or fragment only when the `if` argument is true."
        }....
      ],
      "queryType": {
        "fields": [
          {
            "name": "news",
            "description": " Get News ",
          },
          {
            "name": "posts",
            "description": " Get Posts ",
          },
          {
            "name": "users",
            "description": " Get Users ",
          }
        ]
      },
      "types": [
        {
          "name": "User",
          "description": " User should not be public "
        },
        {
          "name": "News",
          "description": " News is public "
        },
        {
          "name": "Post",
          "description": " Post is public "
        },
        {
          "name": "Query",
          ....
        },
.....
        {
          "name": "__Schema",
          ....
        }
      ]
    }
  }
}

The blog article by my colleague Carlos Eberhardt introduced Attribute-based Access Control rules, and if you've read that, you can probably already see the path to making this work for JWT based access.

Before we go on to defining the policy, let's examine how access policies work: if a rule exists for a field with a condition that evaluates to true, then access is allowed under that condition. The corollary is that if the condition evaluates to false, then access is not allowed. So, we can disallow access to a field simply by using a false condition.

So, a simple effective policy that allows users to access all fields in Query, but not introspection would be:

access:
 policies:
   - type: Query
     rules:
       - name: "Disable introspection"
         condition: false
         fields: [ "__schema", "_type"]
     policyDefault:
       condition: "?$jwt"

Specifically, this allows all users (with a valid JWT) access to all Query fields (e.g. posts, news, and users) and removes access to the fields used for introspection __schema and __type. __type allows users to probe the types and there's really no need for them to do so - unless they are your developers.

So, if you wished to be more nuanced, you could allow your developers access to introspection by adding the following rule:

       - name: "Developer introspection"
         condition: "?$jwt && $jwt.MINE.group: String == 'developers'"
         fields: [ "__schema", "__type"]

This requires that every JWT token have a claim named MINE.group that is added when the token is created.

For public access, you'd likely want to be more controlled about which fields are accessible and a good policy would look like:

access:
 policies:
   - type: Query
     rules:
       - name: "Disable introspection"
         condition: false
         fields: [ "__schema", "_type" ]
       - name: "public fields"
         condition: true
         fields: [ "posts", "news" ]

Summary

Introspection is a core tool when creating and developing a GraphQL endpoint, so StepZen's default is to allow introspection. For production, access policies provide a straight-forward and precise controls over introspection.

NOTE: if you are using your localhost-based proxy to access your StepZen endpoint, it is easy to forget that it is using your Admin key and will bypass any access control policy you have in place.

Controlling introspection is only one tool for managing information leakage. This could be sensitive comments or information that could enable others to use your endpoints in ways you had not anticipated or wanted. However, it's best to have mechanisms in place that protect you even if schema information does leak out.

Consider using access policies to limit access to your GraphQL endpoint. You can also use @materializer, @sequence, @graphql and stepzen import graphql to build out secondary GraphQL endpoints that control and limit your exposure. Check out the StepZen docs for more, and we'll follow up on some of these ideas in future blog articles.