Front-End Web & Mobile

Building a Secure GraphQL API with AWS Amplify and AWS AppSync

Client-side web development with frameworks like React, Angular, and Vue have become incredibly popular. At the same time, GraphQL has emerged as an alternative to REST for building robust, efficient APIs. However, using GraphQL APIs from client-side code comes with unique challenges compared to REST.

One major roadblock is handling Cross-Origin Resource Sharing (CORS) when the API lives on a different domain than the client app. In this post, we’ll explore how to build a GraphQL API with proper CORS configuration using AWS Amplify and AWS AppSync. This will allow calling the API securely from client-side code.

In this article, we explore the integration of Amazon CloudFront with AWS AppSync to enforce domain-specific access on GraphQL APIs, addressing CORS challenges. By leveraging AWS Amplify and AWS AppSync, we streamline CORS configuration and enhance security, fostering an improved user experience. Let’s get started!

High Level Architecture

In this implementation, we bring together Amazon CloudFront with AWS Amplify and AWS AppSync to implement CORS through AWS Cloud Development Kit (CDK).

Prerequisites

To deploy the sample application with CDK, the following will need to be setup:

  • Node Package Manager (NPM), follow the instructions here to install.
  • Use the AWS CLI to configure AWS access credentials with Administrator access so you can deploy resources with the AWS CDK.

Install the AWS CDK Toolkit

The AWS CDK Toolkit, the CLI command cdk, is the primary tool for interacting with your AWS CDK app. It will be used to deploy the CDK code to the target AWS environment. To install, use the command below.

npm install -g aws-cdk

Set up a new AWS CDK Project and install the Amplify GraphQL Construct:

Create a new root project directory amplify-cors-demo. Please refer to this blog for detailed instructions.

mkdir amplify-cors-demo
cd amplify-cors-demo
mkdir backend
cd backend
npx cdk init app --language=typescript
npm install @aws-amplify/graphql-api-construct

In your AWS CDK app’s lib/backend-stack.ts file, import and initialize the new Amplify GraphQL API construct:

import * as path from 'node:path';
import { AmplifyGraphqlApi, AmplifyGraphqlDefinition } from '@aws-amplify/graphql-api-construct';
import * as cdk from 'aws-cdk-lib';
import type { Construct } from 'constructs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const amplifyApi = new AmplifyGraphqlApi(this, "MyNewApi", {
      definition: AmplifyGraphqlDefinition.fromFiles(path.join(__dirname, "schema.graphql")),
      authorizationModes: {
        // FOR TESTING ONLY!
        defaultAuthorizationMode: 'API_KEY',
        apiKeyConfig: {
          expires: cdk.Duration.days(30)
        }
      }
    })
  }
}

Create a schema.graphql file in lib folder with the following content:

#We recommend using 'public' for testing purposes only!

type Blog @model @auth(rules: [{ allow: public }]) {
  title: String
  content: String
  authors: [String]
}

Now, let’s deploy the application with CDK. Answer “y” when prompted:

npx cdk deploy

Now, let’s proceed with building the react-amplified :

cd ..
npm create vite@latest

✔ Project name: … react-amplified
✔ Select a framework: › React
✔ Select a variant: › JavaScript + SWC

cd react-amplified
npm install
npm install aws-amplify

Then we need to configure the Amplify library to be “aware” of our backend API. Go to your app’s entry point (i.e. main.jsx) and configure the Amplify library with the API endpoint information printed from your Terminal when you ran cdk deploy. You should see something like this printed in your last cdk deploy:


Outputs:
BackendStack.amplifyApiModelSchemaS3Uri = s3://backendstack-mynewapiamplifycodegenassetsamplifyco-xxxxxxxxxxxx/model-schema.graphql
BackendStack.awsAppsyncApiEndpoint = https://xxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql
BackendStack.awsAppsyncApiId = 2wxxxxxxxxxxxxxxxxxxxxxxxx
BackendStack.awsAppsyncApiKey = da2-nnxxxxxxxxxxxxxa
BackendStack.awsAppsyncAuthenticationType = API_KEY
BackendStack.awsAppsyncRegion = us-east-1

In your code /react-amplified/src/main.jsx file, import the Amplify library and configure the GraphQL endpoint with the corresponding information.

import { Amplify } from 'aws-amplify'

Amplify.configure({
  API: {
    GraphQL: {
      endpoint: 'https://xxxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql',
      defaultAuthMode: 'apiKey',
      apiKey: 'da2-nnxxxxxxxxxxxxxa',
      region: 'us-east-1',
    }
  }
})

Please refer to Connect a React app to GraphQL and DynamoDB with AWS CDK and Amplify for detailed instructions. Proceed to host your Amplify application.

Set up CloudFront and configure request and response header policies using AWS CDK:

Now, let’s proceed with configuring CloudFront and integrating origin request policies, cache policies, and response policies. This setup will enable requests only from the Amplify hosting domain to access the GraphQL API.

  1. Navigate to your amplify-cors-demo/backend/lib/backend-stack.ts directory using the command line.
  2. Use the template below to add the CloudFront resource and replace the Hosting URL in accessControlAllowOrigins section.

Import dependencies from both CloudFront and Origin. Navigate to the beginning of your file and insert the following import statements:

import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";

Following that, incorporate the subsequent content into the constructor method and replace the AMPLIFY_HOSTING_URL . The provided code establishes policies for Cross-Origin Resource Sharing (CORS) and WebSocket origin requests. It defines CloudFront distribution settings such as origin, permitted methods, caching, and WebSocket behavior. Additionally, it generates outputs facilitating access to distribution details, serving as a valuable resource for reference or information retrieval.

Your backend-stack.ts should appear as follows:

import * as path from "node:path";
import {
  AmplifyGraphqlApi,
  AmplifyGraphqlDefinition,
} from "@aws-amplify/graphql-api-construct";
import * as cdk from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import type { Construct } from "constructs";
import { Code, FunctionRuntime } from "aws-cdk-lib/aws-appsync";

//This is Amplify Hosting URL
export const AMPLIFY_HOSTING_URL = "https://main.dxxxxxxxxx.amplifyapp.com";

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const amplifyApi = new AmplifyGraphqlApi(this, "MyNewApi", {
      definition: AmplifyGraphqlDefinition.fromFiles(
        path.join(__dirname, "schema.graphql")
      ),
      authorizationModes: {
        defaultAuthorizationMode: "API_KEY",
        apiKeyConfig: {
          expires: cdk.Duration.days(30),
        },
      },
    });

    // Start of CloudFront distribution settings
    // Define a response headers policy for CloudFront
    const myResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(
      this,
      "ResponseHeadersPolicyCors",
      {
        responseHeadersPolicyName: "ResponseHeadersPolicyCors",
        comment: "A default policy",
        corsBehavior: {
          accessControlAllowCredentials: false,
          accessControlAllowHeaders: ["content-type", "x-amz-user-agent"],
          accessControlAllowMethods: ["POST", "OPTIONS", "GET"],
          accessControlAllowOrigins: [AMPLIFY_HOSTING_URL],
          originOverride: true,
        },
      }
    );

    // Specify the GraphQL API domain
    const customGraphQLDomain =
      "xxxxxxxxxx.appsync-api.us-east-1.amazonaws.com";

    // Create a WebSocket origin request policy for CloudFront
    const wsOriginRequestPolicy = new cloudfront.OriginRequestPolicy(
      this,
      "webSocketPolicyCors",
      {
        originRequestPolicyName: "webSocketPolicyCors",
        comment: "A default WebSocket policy",
        cookieBehavior: cloudfront.OriginRequestCookieBehavior.none(),
        headerBehavior: cloudfront.OriginRequestHeaderBehavior.allowList(
          "Sec-WebSocket-Key",
          "Sec-WebSocket-Version",
          "Sec-WebSocket-Protocol",
          "Sec-WebSocket-Accept",
          "Sec-WebSocket-Extensions"
        ),
        queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(),
      }
    );

    // Define CloudFront distribution settings
    const distribution = new cloudfront.Distribution(this, "CFDistribution", {
      // Specify domainNames and certificate required for amplify.aws subdomain (connected to a Route53 hosted zone)
      defaultBehavior: {
        origin: new origins.HttpOrigin(customGraphQLDomain, {
          protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER,
        }),
        allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
        cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
        originRequestPolicy: wsOriginRequestPolicy,
        responseHeadersPolicy: myResponseHeadersPolicy,
      },
    });

    // Output CloudFront Distribution ID
    new cdk.CfnOutput(this, "CloudFrontDistributionId", {
      value: distribution.distributionId,
    });

    // Output CloudFront Distribution Domain Name
    new cdk.CfnOutput(this, "CloudFrontDistributionDomainName", {
      value: distribution.distributionDomainName,
    });

    // Output CloudFront Distribution URL
    new cdk.CfnOutput(this, "CloudFrontDistributionURL", {
      value: `https://${distribution.distributionDomainName}`,
    });
    //End of CloudFront distribution settings
  }
}

Run cdk deploy.

This action will configure CloudFront with a rule that permits requests exclusively from the Amplify Hosting domain.

Update Amplify App Configuration:

Then update the Amplify app configuration using CloudFront Distribution Domain name. Go to your app’s entry point (i.e. main.jsx) and configure the Amplify library with the API endpoint information printed from your Terminal when you ran cdk deploy. You should see something like this printed in your last cdk deploy:

Outputs:
BackendStack.CloudFrontDistributionDomainName = xxxxxxxx.cloudfront.net
BackendStack.CloudFrontDistributionId = EXXXXXXXXXXX
BackendStack.CloudFrontDistributionURL = https://xxxxxxxx.cloudfront.net
BackendStack.amplifyApiModelSchemaS3Uri = s3://backendstack-mynewapiamplifycodegenassetsamplifyco-xxxxxxxxxxxx/model-schema.graphql
BackendStack.awsAppsyncApiEndpoint = https://xxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql
BackendStack.awsAppsyncApiId = 2wxxxxxxxxxxxxxxxxxxxxxxxx
BackendStack.awsAppsyncApiKey = da2-nnxxxxxxxxxxxxxa
BackendStack.awsAppsyncAuthenticationType = API_KEY
BackendStack.awsAppsyncRegion = us-east-1

Configure the GraphQL endpoint in /react-amplified/src/main.jsx. and update replace the CloudFrontDistributionDomainName in the endpoint.

import { Amplify } from 'aws-amplify'

Amplify.configure({
  API: {
    GraphQL: {
      endpoint: 'https://<CloudFrontDistributionDomainName>/graphql',
      defaultAuthMode: 'apiKey',
      apiKey: 'da2-nnxxxxxxxxxxxxxa',
      region: 'us-east-1',
    }
  }
})

Deploy and Test

  • Deploy the Amplify app.
  • Test your application and ensure the only request from the Hosting domain are allowed to access the GraphQL API.

Before: The images provided demonstrates that there are no domain restrictions to access the GraphQL API.

From Hosting domain:

From localhost:

After: The image presented below demonstrates a GraphQL API call from the hosting domain that has been successful.

The image indicates that API requests from the localhost domain encountered CORS errors.

Clean up

To clean up all the generated resources. Run the following AWS CDK CLI command in your terminal:

cdk destroy

Conclusion

In this blogpost, we’ve combined Amazon CloudFront with AWS AppSync, enabling us to restrict access to the GraphQL APIs to specific domains effectively. This integrated solution simplifies CORS configuration, strengthens security, and enhances the overall user experience. With seamless communication between your AWS Amplify app and AWS AppSync API, you can confidently develop and deploy robust applications.  Join our Community Discord to leave any feedback or feature requests.

Anil Maktala

Anil Maktala is a Developer Experience Engineer (Solutions Architect) at AWS Amplify, bringing extensive experience in developing a diverse range of applications, products, and architectural solutions. He is deeply committed to assisting customers in designing scalable solutions tailored to their unique needs.

Josef Aidt

Josef is a Developer Experience Engineer working with AWS Amplify’s open-source CLI. He’s passionate about solving complex customer problems in the front-end domain and addresses real-world architecture problems for development using front-end technologies. You can follow Josef on Twitter at @josefaidt

Arundeep Nagaraj

Arundeep Nagaraj is a Developer Experience Manager for AWS Amplify providing developers guidance on front-end, web and mobile architectures and resolving issues in Open Source libraries. You can follow Arundeep on Twitter – @arun_deepn