Front-End Web & Mobile

Supporting backend and internal processes with AWS AppSync multiple authorization types

Imagine a scenario where you created a mobile or web application that uses a GraphQL API built on top of AWS AppSync and Amazon DynamoDB tables. Another backend or internal process such as an AWS Lambda function now needs to update data in the backend tables. A new feature in AWS AppSync lets you grant the Lambda function access to make secure GraphQL API calls through the unified AppSync API endpoint.

This post explores how to use the multiple authorization type feature to accomplish that goal.

Overview

In your application, you implemented the following:

  1. Users authenticate through Amazon Cognito user pools.
  2. Users query the AWS AppSync API to view your data in the app.
  3. The data is stored in DynamoDB tables.
  4. GraphQL subscriptions reflect changes to the data back to the user.

Your app is great. It works well. However, you may have another backend or internal process that wants to update the data in the DynamoDB tables behind the scenes, such as:

  • An external data-ingestion process to an Amazon S3 bucket
  • Real-time data gathered through Amazon Kinesis Data Streams
  • An Amazon SNS message responding to an outside event

For each of these scenarios, you want to use a Lambda function to go through a unified API endpoint to update data in the DynamoDB tables. AWS AppSync can serve as an appropriate middle layer to provide this functionality.

Walkthrough

An Amazon Cognito user pool authenticates and authorizes your API. Keep this in mind when considering the best way to grant the Lambda function access to make secure AWS AppSync API calls.

Choosing an authorization mode

AWS AppSync supports four different authorization types:

  • API_KEY: For using API keys
  • AMAZON_COGNITO_USER_POOLS: For using an Amazon Cognito user pool
  • AWS_IAM: For using IAM permissions
  • OPENID_CONNECT: For using your OpenID Connect provider

Before the launch of the multiple authorization type feature, you could only use one of these authorization types at a time. Now, you can mix and match them to provide better levels of access control.

To set additional authorization types, use the following schema directives:

  • @aws_api_key — A field uses API_KEY for authorization.
  • @aws_cognito_user_pools — A field uses AMAZON_COGNITO_USER_POOLS for authorization.
  • @aws_iam — A field uses AWS_IAM for authorization.
  • @aws_oidc — A field uses OPENID_CONNECT for authorization.

The AWS_IAM type is ideal for the Lambda function because the Lambda function is bound to an IAM execution role where you can specify the permissions this Lambda function can have. Do not use the API_KEY authorization mode; API keys are only recommended for development purposes or for use cases where it’s safe to expose a public API.

Understanding the architecture

Suppose that you have a log viewer web app that lets you view logging data:

  • It authenticates its users using an Amazon Cognito user pool and accesses an AWS AppSync API endpoint for data reads from a “Log” DynamoDB table.
  • Some backend processes publish log events and details to an SNS topic.
  • A Lambda function subscribes to the topic and invokes the AWS AppSync API to update the backend data store.

The following diagram shows the web app architecture.

The following code is your AWS AppSync GraphQL schema, with no authorization directives:

type Log {
  id: ID!
  event: String
  detail: String
}

input CreateLogInput {
  id: ID
  event: String
  detail: String
}

input UpdateLogInput {
  id: ID!
  event: String
  detail: String
}

input DeleteLogInput {
  id: ID!
}

type ModelLogConnection {
  items: [Log]
  nextToken: String
}

type Mutation {
  createLog(input: CreateLogInput!): Log
  updateLog(input: UpdateLogInput!): Log
  deleteLog(input: DeleteLogInput!): Log
}

type Query {
  getLog(id: ID!): Log
  listLogs: ModelLogConnection
}

type Subscription {
  onCreateLog: Log
    @aws_subscribe(mutations: ["createLog"])
  onUpdateLog: Log
    @aws_subscribe(mutations: ["updateLog"])
  onDeleteLog: Log
    @aws_subscribe(mutations: ["deleteLog"])
}

Configuring the AWS AppSync API

First, configure your AWS AppSync API to add the new authorization mode:

  • In the AWS AppSync console, select your API.
  • Under the name of your API, choose Settings.
  • For Default authorization mode, make sure it is set to Amazon Cognito user pool.
  • To the right of Additional authorization providers, choose New.
  • For Authorization mode, choose AWS Identity and Access Management (IAM), Submit.
  • Choose Save.

Now that you’ve set up an additional authorization provider, modify your schema to allow AWS_IAM authorization by adding @aws_iam to the createLog mutation. The new schema looks like the following code:

input CreateLogInput {
  id: ID
  event: String
  detail: String
}

input UpdateLogInput {
  id: ID!
  event: String
  detail: String
}

input DeleteLogInput {
  id: ID!
}

type ModelLogConnection {
  items: [Log]
  nextToken: String
}

type Mutation {
  createLog(input: CreateLogInput!): Log
    @aws_iam
  updateLog(input: UpdateLogInput!): Log
  deleteLog(input: DeleteLogInput!): Log
}

type Query {
  getLog(id: ID!): Log
  listLogs: ModelLogConnection
}

type Subscription {
  onCreateLog: Log
    @aws_subscribe(mutations: ["createLog"])
  onUpdateLog: Log
    @aws_subscribe(mutations: ["updateLog"])
  onDeleteLog: Log
    @aws_subscribe(mutations: ["deleteLog"])
}

type Log @aws_iam {
  id: ID!
  event: String
  detail: String
}

The @aws_iam directive is now authorizing the createLog mutation. Add the directive to the log type. Because directives work at the field level, also give AWS_IAM access to the log type. To do this, either mark each field in the log type with a directive or mark the log type with the @aws_iam directive.

You don’t have to explicitly specify the @aws_cognito_user_pools directive, because it is the default authorization type. Fields that are not marked by other directives are protected using the Amazon Cognito user pool.

Creating a Lambda function

Now that the AWS AppSync backend is set up, create a Lambda function. The function is triggered by an event published to an SNS topic, which contains logging event and detail information in the message body.

The following code example shows how the Lambda function is written in Node.js:

require('isomorphic-fetch');
const AWS = require('aws-sdk/global');
const AUTH_TYPE = require('aws-appsync').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
const gql = require('graphql-tag');

const config = {
  url: process.env.APPSYNC_ENDPOINT,
  region: process.env.AWS_REGION,
  auth: {
    type: AUTH_TYPE.AWS_IAM,
    credentials: AWS.config.credentials,
  },
  disableOffline: true
};

const createLogMutation =
`mutation createLog($input: CreateLogInput!) {
  createLog(input: $input) {
    id
    event
    detail
  }
}`;

const client = new AWSAppSyncClient(config);

exports.handler = (event, context, callback) => {

  // An expected payload has the following format:
  // {
  //   "event": "sample event",
  //   "detail": "sample detail"
  // }

  const payload = event['Records'][0]["Sns"]['Message'];

  if (!payload['event']) {
    callback(Error("event must be provided in the message body"));
    return;
  }

  const logDetails = {
    event: payload['event'],
    detail: payload['detail']
  };

  (async () => {
    try {
      const result = await client.mutate({
        mutation: gql(createLogMutation),
        variables: {input: logDetails}
      });
      console.log(result.data);
      callback(null, result.data);
    } catch (e) {
      console.warn('Error sending mutation: ',  e);
      callback(Error(e));
    }
  })();
};

The Lambda function uses the AWS AppSync SDK to make a createLog mutation call, using the AWS_IAM authorization type.

Defining the IAM role

Now, define the IAM role that this Lambda function can assume. Grant the Lambda function appsync:GraphQL permissions for your API, as well as Amazon CloudWatch Logs permissions. You also must allow the Lambda function to be triggered by an SNS topic.

You can view the full AWS CloudFormation template that deploys the Lambda function, its IAM permissions, and supporting resources:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  GraphQLApiEndpoint:
    Type: String
    Description: The https endpoint of an AppSync API
  GraphQLApiId:
    Type: String
    Description: The id of an AppSync API
  SnsTopicArn:
    Type: String
    Description: The ARN of the SNS topic that can trigger the Lambda function
Resources:
  AppSyncSNSLambda:
    Type: 'AWS::Serverless::Function'
    Properties:
      Description: A Lambda function that invokes an AppSync API endpoint
      Handler: index.handler
      Runtime: nodejs8.10
      MemorySize: 256
      Timeout: 10
      CodeUri: ./
      Role: !GetAtt AppSyncLambdaRole.Arn
      Environment:
        Variables:
          APPSYNC_ENDPOINT: !Ref GraphQLApiEndpoint

  AppSyncLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service: lambda.amazonaws.com
          Action: sts:AssumeRole
      Policies:
      - PolicyName: AppSyncLambdaPolicy
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Resource: arn:aws:logs:*
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
          - Effect: Allow
            Resource:
            - !Sub 'arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${GraphQLApiId}*'
            Action:
            - appsync:GraphQL

  SnsSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !GetAtt AppSyncSNSLambda.Arn
      Protocol: Lambda
      TopicArn: !Ref SnsTopicArn

  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref AppSyncSNSLambda
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref SnsTopicArn

Deploying the AWS CloudFormation template

Use the following two commands to deploy the AWS CloudFormation template. Make sure to replace all the CAPS fields with values specific to your AWS account:

aws cloudformation package --template-file "cloudformation.yaml" \
  --s3-bucket "<YOUR S3 BUCKET>" \
  --output-template-file "out.yaml"

aws cloudformation deploy --template-file out.yaml \
    --stack-name appsync-lambda \
    --s3-bucket "<YOUR S3 BUCKET>" \
    --parameter-overrides GraphQLApiEndpoint="<YOUR GRAPHQL ENDPOINT>" \
      GraphQLApiId="<YOUR GRAPHQL API ID>" \
      SnsTopicArn="<YOUR SNS TOPIC ARN>" \
    --capabilities CAPABILITY_IAM

Testing the solution

After both commands succeed, and your AWS CloudFormation template deploys, do the following:

1. Open the console and navigate to the SNS topic that you specified earlier.
2. Choose Publish message.
3. For the raw message body, enter the following:

{
   "event": "sample event",
   "detail": "sample detail"
}

4. Choose Publish message.

Navigate to the Log DynamoDB table that is your AWS AppSync API’s data source. You should see a new “sample event” record created using the CreateLog mutation.

Conclusion

With its new feature, AWS AppSync can now support multiple authorization types. This ability demonstrates how an AWS AppSync API serves as a powerful middle layer between multiple processes while being a secure API for end users.

As always, AWS welcomes feedback. Please submit comments or questions below.

Jane Shen is a cloud application architect in AWS Professional Services based in Toronto, Canada.