GraphQL

Build a GraphQL Client Application to Consumer Protected GraphQL API Resources Part 2

This article is part 2 of our GraphQL application protection series. In this article, we will build a GraphQL API server and protect its resources with externalized policies administered in the Cloudentity Authorization SaaS platform. We will also protect the GraphQL API endpoint data with a local policy enforcement/decision point for the app deployed within a local Kubernetes cluster. This approach will also showcase a modern application protection hybrid model with local enforcement and Cloud based authorization and policy administration.

I Am Looking for Part 1

You can find the first part of the Build a GraphQL Client Application to Consumer Protected GraphQL API Resources series here.

Overview

We will build the GraphQL API server with express-graphql and lokijs as a built-in database. Our application would be a tweet service that serves and consumes data exposed through APIs as per GraphQL specification. Once we build the application, we will deploy it to a local kubernetes cluster using kind and enforce centralized and decoupled policy based authorization without modifying any business logic or code.

Cloudentity authorization

You can checkout this entire demo application and related integration source here

Build the GraphQL server application

Pre-requisites

Following tools are required to build this application. nodejs was picked for its simplicity to build apps.

  • nodejs - Recommended v16.0 +
  • npm - Recommended v8.3.0 +

SKIP/JUMP LEVEL

In case you are not interested in building the application from scratch, you can skip some of the steps below and instead checkout/clone the Cloudentity Samples GraphQL Demo GitHub repository to get the application source code.

git clone git@github.com:cloudentity/ce-samples-graphql-demo.git

Then, follow the instructions to continue with the Build and Deploy Section

Initialize a Node.js Project

  1. Initialize a Node.js project.

    mkdir tweet-service-graphql-nodejs && cd tweet-service-graphql-nodejs
    npm init
    
  2. Click ENTER with no input for all prompts during the npm init and finally type yes for the OK prompt. This creates a package.json file for the project that holds the dependencies and other execution script commands.

  3. Add a start command to scripts section in the package.json file to start the application quickly, e.g.:

    {
    ..
    "scripts": {
       "test": "echo \"Error: no test specified\" && exit 1",
       "start": "node index.js"
    },
    ..
    }
    
  4. We will use the express npm module for HTTP handling, so let’s install the dependency as well.

    npm install --save express
    

Add HTTP Routers and Listeners

Create a file named index.js and add a basic router and listeners.

var express = require('express');
var app = express();

app.get('/', function(req, res) {
 res.send("We are building a tweet service!")
});

app.get('/health', function(req, res) {
 res.send('Service is alive and healthy')
});

app.listen(5001);
console.log("Server listening at http://localhost:5001/");

Run the Application

Run the application by executing the below command in your terminal:

npm start

This starts and serves the Node.js application listener, and the endpoints below should be serving traffic:

Now that we have the basic structure in place, let’s continue to add some GraphQL specific features to the Node.js application.

Add GraphQL Capabilites

To add GraphQL API compliant endpoint within the Node.js server application, we will use the express-graphql npm package.

  1. Let’s install the dependencies and attach a listener endpoint for graphQL.

    npm install --save graphql@15.3.0 express-graphql@0.12.0
    
  2. Add a schema specification to the index.js file.

    GraphQL SDL allows defintion of the GraphQL schema using various constructs as defined in the GraphQL specification. Let’s build a schema that has a mixed flavor of basic object types, fields, query and mutations.

    In the schema, we will add a mix of GraphQL constructs and, later in the article, we will explore how to attach externalized authorization policies to each of these GraphQL constructs.

    GraphQL Construct Examples
    Object Type TweetInput, Tweet
    Field content, author, id, dateModified, dateCreated ..
    Query sayHiTweety, getTweet, getLatestTweets ..
    Mutation createTweet, updateTweet, deleteTweet ..

    Add below schema specification to index.js

    // graphql package import
    var { graphqlHTTP } = require('express-graphql');
    var { buildSchema } = require('graphql');
    
    // graphql schema definition
    var schema = buildSchema(
    `
    input TweetInput {
    content: String
    author: String
       }
    
    type Tweet {
    id: String
    content: String
    dateCreated: String
       dateModified: String
    author: String
    }
    
    type Query {
    sayHiTweety: String
    getTweet(id: String!) : Tweet
    getLatestTweets : [Tweet]
    }
    
    type Mutation {
    createTweet(tweet: TweetInput): Tweet
    updateTweet(id: String!, tweet: TweetInput): Tweet
       deleteTweet(id: String!): String
    }`
    );
    
  3. Add implementation & listener for a GraphQL query

    Let’s add a simple resolver implementation for the first query sayHiTweety. We will expand to other query and mutation implementations later in the article. In contrast to REST, GrpahQL is designed to be a single endpoint API system. All the queries and mutations will be served over this single enpoint at /graphql. The URL name can be anything, graphql is just used for convenience.

    
    var resolverRoot = {
    sayHiTweety: () => {
    return 'Hello Tweety';
       }
    };
    
    app.use('/graphql', graphqlHTTP(
    {
    schema: schema,
    rootValue: resolverRoot,
    graphiql: true
    
    }
    ));
    
    
  4. Verify GraphQL API operations.

    Start the application using the npm start command and launch the GraphQL endpoint at http://localhost:5001/graphql. Since the graphiql flag is set to true, we will see an interactive query screen and schema explorer as the response.

    Important

    We will not be using above interface but usage of Postman application for further verification of GraphQL APIs is recommended.

    You can download the Postman application, in case you don’t have it already, and then import the Cloudentity GraphQL tweet service demo postman collection into Postman.

  5. Execute GraphQL sayHiTweety query.

    Navigate to the imported Postman collection under request-noauth folder and run the sayHiTweety-Query GraphQL API request. It should respond with the response attached below.

    {
       "data": {
          "sayHiTweety": "Hello Tweety"
       }
    }
    

    Graphql-api-response

Expand GraphQL Implementation Logic

Let’s add a simple in-memory datastore to store the tweets and then add some mutations and queries to act on those objects. We will not dive into the specifics or add any complex business logic. Our main goal is to showcase externalized authorization policy administration and enforcement for the various GraphQL objects, fields, queries, mutations, and more at runtime.

  1. Install the dependencies.

    npm install --save uuid lokijs
    
  2. Add implementation for the GraphQL query and mutation.

    //imports
    var loki = require('lokijs');
    const {v4: uuidv4} = require('uuid');
    
    //logic
    var db = new loki('tweets.db');
    var tweets = db.addCollection('tweets');
    
    const getTweet = (tid) => {
    console.log("Fetching record..");
    var results = tweets.find({id: tid.id});
    var res = results.length > 0 ? results[0] : null
    return res;
    }
    
    const storeTweet = (t) => {
    tweets.insert(
    {
       id: t.id,
       content: t.content,
       author: t.author,
       dateCreated: t.dateCreated
    }
    );
    console.log(tweets);
    return t;
    }
    
    function Tweet(input) {
    this.id = uuidv4();
    this.content = input.tweet.content;
    this.author = input.tweet.author;
    this.dateCreated = new Date().toLocaleString();
    this.dateModified = new Date().toLocaleString();
    }
    
    var resolverRoot = {
    sayHiTweety: () => {
       return 'Hello Tweety';
       },
       getTweet: (tid) =>  {
    console.log("Fetching tweet using id: " + Object.values(tid));
    return getTweet(tid);
    },
    createTweet: (input) =>  {
    console.log("Creating a new tweet...");
    const newTweet = new Tweet(input);
    storeTweet(newTweet);
    return newTweet;
    },
    getLatestTweets: () => {
    console.log("Fetching records..");
    var tweets = db.getCollection('tweets');
    var all = tweets.find({ 'id': { '$ne': null } });
    return all;
    },
    deleteTweet: (tid) => {
    console.log("Deleting tweet..");
    var tweets = db.getCollection('tweets');
    tweets.findAndRemove({id: tid.id});
    return tid.id;
    },
    };
    
  3. Verify all GraphQL API operations.

    Navigate to the imported collection in Postman and run rest of the GraphQL endpoints. These should now return responses similar to below attached samples. At this point the GraphQL API application is completely unprotected and data can be requested or posted without authorization.

    Graphql-api-responses

Deploy and Run GraphQL API Rorkload in Kubernetes Cluster

Let’s deploy the GraphQL API onto a local Kubernetes cluster to enforce externalized authorization policies with the Cloudentity authorization platform without any modification to the application code.

Makefile

We will be referencing make commands in the below sections. There is a Makefile in the project folder and the contents can be inspected for the actual commands that are orchestrated by the make target.

Prerequisites

Local Kubernetes cluster can be deployed using any tool of your choice, but we will use kind in this article.

SKIP/JUMP LEVEL

In case you want to skip some of the deployment detailed steps and want to move to next logical step with a shortcut, use the following command:

make all

This will deploy all resources and you can jump to the Protect using Cloudentity authorization platform.

Build the Docker Image

  1. Get a copy of the Makefile and Dockerfile.

    wget https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/blob/master/tweet-service-graphql-nodejs/Makefile
    wget https://raw.githubusercontent.com/ce-samples-graphql-demo/blob/master/tweet-service-graphql-nodejs/Dockerfile
    
  2. Now, let’s build the Docker image for the GraphQL API using:

    make build-image
    

Launch the Kubernetes Cluster

We will go ahead and create a Kubernetes cluster using kind. The below cluster config will be used to create the cluster and within the config, a NodePort is configured to enable accessibility at port 5001 from outside of the cluster since we do not have an actual load balancer.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 31234
    hostPort: 5001
    protocol: TCP
  1. Get a copy of k8s-cluster-config.yaml

    wget https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/k8s-cluster-config.yaml
    
  2. Create the kind cluster using:

    make deploy-cluster
    

Deploy GraphQL Application on the Kubernetes Cluster

We will use Helm (a package manager for Kubernetes) to define and deploy all the Kubernetes resources required for the application. We will not be going into the Helm details, so copy the existing helm templates into our working directory from Cloudentity Samples GraphQL Demo.

Using the below make command, upload the image to the kind cluster, create a Kubernetes namespace, and deploy the GraphQL app.

make deploy-app-graph-ns

The above make target launches all the pods and services to run the GraphQL application. The status of the pods and services can be fetched using:

kubectl get pods -n svc-apps-graph-ns

kubectl get services -n svc-apps-graph-ns

Service Readiness

Let’s execute into the pod container to see if the service is reachable (note the ID appended to the name of the pod after running kubectl get pods -n svc-apps-graph-ns and replace it in the command below)

kubectl exec -it <pod-name> -n svc-apps-graph-ns -- /bin/sh

Now, you can run the following CURL request:

curl --location --request POST 'http://local.cloudentity.com:5001/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query {\n    sayHiTweety\n}\n","variables":{}}'

After the request, the application should respond with the following data:

{
    "data": {
        "sayHiTweety": "Hello Tweety"
    }
}

Network Access

The components deployed on the Kubernetes cluster are not exposed outside the Kubernetes cluster. External access to individual services can be provided by creating an external load balancer or node port on each service. An Ingress Gateway resource can be created to allow external requests through the Istio Ingress Gateway to the backing services.

Deploy Istio

To allow external service access, let’s install Istio onto this cluster. We will use the Helm based install mechanism for installing Istio We have condensed all required steps under the make target, which updates the Helm repository and installs istiod under the istio-system namespace.

make deploy-istio

Check the status of the pods:

kubectl get pods -n istio-system

Once the pod is healthy, let’s add an Istio Ingress Gateway to expose the traffic outside the cluster.

Expose the Service Externally with Istio Ingress Gateway

Let’s install the Istio Ingress Gateway and configure a Virtual Service for routing to the GraphQL service in the svc-apps-graph-ns namespace.

  1. Let’s first copy some configs that will be used for the below steps from the GitHub repository:

    mkdir istio-configs
    wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-helm-config-override.yaml
    wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-ingress-gateway-graphql.yaml
    wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-ingress-virtual-service.yaml
    wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-mp-authorizer-policy.yaml
    
  2. Deploy the Istio Ingress Gateway using:

    make deploy-istio-gateway
    

    You can confirm if the gateway and virtual service is created and running using

    kubectl get gateways -A
    kubectl get virtualservices -A
    
  3. Now, let’s check to see if we can access the service from outside the cluster by executing one (or both) of the following commands:

    curl --location --request POST 'http://localhost:5001/graphql' \
    --header 'Host: local.cloudentity.com' --header 'Content-Type: application/json' \
    --data-raw '{"query":"query {\n    sayHiTweety\n}\n","variables":{}}'
    

    OR

    curl --location --request POST 'http://local.cloudentity.com:5001/graphql' \
    --header 'Content-Type: application/json' \
    --data-raw '{"query":"query {\n    sayHiTweety\n}\n","variables":{}}'
    

    Notice that the either the Host header needs to be passed in or the domain needs to have a matching name of local.cloudentity.com as it’s defined in the Istio Virtual Service routing rule.

    Expected output:

    {
       "data": {
          "sayHiTweety": "Hello Tweety"
       }
    }
    

    Result

    Voila! Now the services should be accessible from outside the cluster. Now that we have a production-like Kubernetes deployment serving GraphQL operations from the platform, let’s dive into protecting the application using externalized authorization policies using the Cloudentity platform without altering the application code at all.

Authorization Policy Administration in Cloudentity Authorization Platform

Sign up for a free Cloudentity Authorization SaaS account.

Activate the tenant and take the self guided tour to familiarize yourself with the platform.

Now that you have the Cloudentity platform available, let’s connect all the pieces together as shown below:

Cloudentity istio authorizer

Annotate Services for Service Auto Discovery

Cloudentity Authorizers can self-discover API endpoints if annotated properly in a Kubernetes cluster. More details on discovery can be found in auto discovery of services on Istio.

For example, in the Helm chart, we have annotated the services so that final deployment resource file has the annotations. services.k8s.cloudentity.com/spec-url annotation enables this GraphQL schema to be read by Cloudentity Istio Authorizers deployed onto a cluster and then propagated to the Cloudentity plaform can then govern and attach declarative policies on the GraphQL schema itself.

Note

The GraphQL schema URL can be served by the GraphQL API resource server itself or hosted in separate accessible location. We are using an external URL only for demonstration purpose.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "tweet-service-graphql-nodejs.fullname" . }}
  labels:
    {{- include "tweet-service-graphql-nodejs.labels" . | nindent 4 }}
  annotations:
    services.k8s.cloudentity.com/spec-url: "https://raw.githubusercontent.com/cloudentity/random-bin/master/graphql/tweet-svc-schema"
    services.k8s.cloudentity.com/graphql-path: "/graphql"

In case you want to change the annotation, you can update the url in the Helm template and do the following:

helm uninstall svc-apps-graphql -n svc-apps-graph-ns
helm install svc-apps-graphql helm-chart/tweet-service-graphql-nodejs -n svc-apps-graph-ns

Deploy Cloudentity Istio Authorizer to the Kubernetes Cluster

In this step, we will install and configure the Cloudentity Istio Authorizer to act as the Policy Decision Point(PDP). The scope of responsibility of this component is to act as the local policy decision point within the Kubernetes cluster. This component is also responsible to pull down all the applicable authorization policies authored and managed within the Cloudentity authorization platform. In the below image, the highlighted section in the box is the component that we will download and install onto a local Kubernetes cluster.

Cloudentity istio authorizer

Detailed Istio setup concepts and instruction are available here, but, in a nutshell, the steps are:

  1. Navigate to the Cloudentity authorization platform admin console.

  2. Go to Authorization » Gateways, select Create Gateway, and create a new Istio authorizer.

  3. Select Create and bind services automatically.

    Technically, this means we will register this discovered service as an OAuth resource server within the Cloudentity platform.

  4. Install the istio-authorizer in target Kubernetes using the helm commands provided in the Quickstart installation instructions Step 1 (make sure to select the visibility icon to make the secrets visible before copying the commands).

    You will also need to edit the helm upgrade command to add the namespace svc-apps-graph-ns to the list of discovery namespaces, as shown below:

    helm upgrade --install istio-authorizer acp/istio-authorizer \
    ...
    --set "discovery.namespaces={default,svc-apps-graph-ns}"\
    ...
    

    Note

    Cloudentity Istio Authorizer will be deployed to its own name space (in this case acp-istio-authorizer). The authorizers can be deployed to any namespace based on the deployment architecture. The namespaces chosen in this article are for demonstration purpose only.

    What happens with the given helm command? It:

    Cloudentity istio authorizer authorization

    You can modify the command to include an override in the values file

    parseBody:
      enabled: true
    
    helm upgrade --install istio-authorizer acp/istio-authorizer \
    -f overide-values.yaml
    ..
    ..
    

    Important

    If you don’t apply the above request parser sidecar, you will get the following error:

    {
       "errors": [
            {
                "message": "failed to parse json: unexpected end of JSON input"
            }
        ]
    }
    
  5. Attach external authorization to Istio:

    Cloudentity Istio Authorizer is designed to be a native Istio extension that uses the Istio External authorizer model. Following the Step 2 instructions under the Quickstart tab, add extensionProviders under mesh section to indicate that acp-authorizer will be an external authz provider.

  6. Confirm if your authorizer deployment is healthy:

    kubectl get pods -n acp-istio-authorizer
    

    Cloudentity istio authorizer authorization

    Important

    We have seen issues with this step if there is network traffic restrictions between the local workstations and the external Cloudentity platform due to internal firewalls, etc. Make sure the traffic path is allowed in case you see the pod status as not healthy.

Restart the Service Pods

Since we are using automatic Istio injection, we need to recreate the pod so that envoy-proxy is injected into the service namespaces:

kubectl rollout restart deployment/svc-apps-graphql-tweet-service-graphql-nodejs -n svc-apps-graph-ns

After this step we should see the APIs auto discovered by the Cloudentity Istio Authorizer and propagated back up to the the Cloudentity Authorization SaaS platform. Let’s check it out in the Cloudentity authorization platform.

Cloudentity Authorizer and Cloudentity Platform Communication

It is very important that the communication path between the local Istio Authorizer and Cloudentity Authorization SaaS platform is established. The local Istio Authorizer is responsible for pushing any discovered services to platform for further governance. Once pushed, the centralized policies administered and managed in the platform are polled back by the authorizer for policy decisions and enforcement locally.

Cloudentity istio authorizer authorization

Service Communication

  1. Login into the Cloudentity authorization admin portal.

  2. Navigate to the Authorization » Gateways.

    As shown in the below diagram, the Last Active column is an indication of communication status of the local authorizer with the remote platform.

    Cloudentity istio authorizer authorization

    Regarding the communication security, the local Istio authorizer uses OAuth authorization mechanisms to authenticate itself to the Cloudentity authorization SaaS platform before handshaking information.

Discovered Service from Cluster

If the business service(workload) is discovered from the remote Kubernets cluster, it should automatically be bound in the Cloudentity platform. Technically, this means Cloudentity authorization platform registers this discovered service as an OAuth resource server and can be governed from within the platform).

Cloudentity istio authorizer authorization

Govern GraphQL API and Schema

At this point, the remote workload should be available to be governed within the Cloudentity authorization platform. In case of the GraphQL workload, the schema annotated along with the workload is also transferred by the local Cloudentity Istio Authorizer to the Cloudentity authorization platform. This enables us to explore the GraphQL schema for the workload and we can apply authorization policies and manage the policies applied to the various constructs within the GraphQL schema.

Cloudentity istio authorizer authorization

Cloudentity istio authorizer authorization

Now we have seen the service is available in the Cloudentity authorization platform for governance and central management of authorization policies, which will automatically be downloaded by respective local satelite Cloudentity authorizers bound to the services. This way, the Cloudentity authorization platform acts as a very powerful and robust policy management services engine and the Cloudentity authorizers acts as policy runtime services that makes decisions using the policies governed and administered within the central policy management engine.

Enforce externalized dynamic authorization

Before we start enforcing policies, run the postman collection to check if all the GraphQL API operations are still accessible.

Once the pre check is complete, let’s try to add more authorization scenarios to enforce access and authorization policies authored and managed via the Cloudentity authorization platform.

For policy governance, we expect the admin (policy administrator) to:

  • Login into the Cloudentity Authorization portal.

  • Navigate to the workspace and select Enforcement > APIs to see the GraphQL APIs.

  • Apply policies at various GraphQL construct level in the GraphQL API explorer.

Scenario #1: Block GraphQL endpoint alltogether

Let’s say we want to temporarily block access to all users for this endpoint. Select the GraphQL API endpoint and attach a Block API policy.

This, in effect, blocks any call made to any GraphQL operation. Run any of the tests in the imported Postman Test collection and we should see an “authorization denied” response.

Authorization policy Output
alt-text-1 alt-text-2
alt-text-1 alt-text-2

Scenario #2: Disallow GraphQL Queries for Specific Fields

Let’s say we do not want GraphQL clients to ask for a specific field that is available in the schema. These could be internal identifiers or only could be requested by some special priviliged internal applications. For this:

  1. Select the GraphQL API endpoint and enter into the GraphQL API explorer.

  2. Navigate to Objects and go to the Tweet field.

  3. Assign the Block API policy to dateModified completely.

This, in effect, blocks any GraphQL query that requests for the dateModified field.

Let’s run the getLatestTweets query from the imported Postman Test collection.

Modify the request payload to include/exclude dateModified in the query and observe the response difference. Whenever dateModified is requested, the request is automatically rejected.

Authorization policy Output
alt-text-1 alt-text-2
alt-text-1 alt-text-2

Scenario #3: Disallow GraphQL Queries for Specific Objects with Constraints

Let’s say we do not want GraphQL clients to ask for a specific object unless some specific constraints are met for that client application. For example, we want to return the Tweet object only when some constraint is met. For this, we can apply a policy at the object level for IP address check that is available in the given range. We will use the inbuilt REGO engine to author the policy.

For this:

  1. Select the GraphQL API endpoint and enter into the GraphQL API explorer

  2. Navigate to Objects

  3. Select the Tweet object and apply the constrained IP check policy at object level.

Sample Rego policy:

package acp.authz

default allow = false

allowedCidrRange :=
    [
    "3.0.0.0/9",
    "3.128.0.0/10",
    "4.0.0.0/8",
    "5.8.63.0/32",
    "5.10.64.160/29",
    "217.161.27.0/25"
    ]

extracted_ip := input.request.headers["X-Custom-User-IP"][_]


is_within_allowed_cidr = true {
    some i
    net.cidr_contains(allowedCidrRange[i], extracted_ip)
} else = false

allow {
  is_within_allowed_cidr
}
Authorization policy Output
alt-text-1 alt-text-2

Scenario #4: Block GraphQL Delete Mutation Unless It Comes from Client with Specific Metadata

Let’s say we do not want all GraphQL clients to operate on the deleteTweet mutation. For example, we want to allow only access tokens issued to specific clients to be authorized to use the deleteTweet mutation. This way this operation can be used by specific clients and not all client apps even though it is available in schema.

For this:

  1. Select the GraphQL API endpoint and enter into the GraphQL API explorer

  2. Navigate to Mutation

  3. Select the deleteTweet Mutation operation and apply the constrained allow-only-for-specific-clients policy at mutation level.

Authorization policy Output
alt-text-1 alt-text-2

Scenario #5: Allow GraphQL Query Only if Token Is Issued by Specific Authorization Server

Let’s say we do not want all GraphQL clients to operate on the getTweets query.

For example, we want to allow only access tokens issued by specific authorization servers to be authorized to use the getTweets query.

alt-text-1

Authorization policy Output
alt-text-1 alt-text-2

Next steps

Now that we have protected a GraphQL API resource server with dynamic and flexible authorization policies, we will build a simple GraphQL client application to demonstrate an entire application in real life. In this client application, we will look at how to get an access token from the Cloudentity authorization server and, then, utilize it to make further calls to the GraphQL API resource server.

Updated: Nov 2, 2023