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:
- Users authenticate through Amazon Cognito user pools.
- Users query the AWS AppSync API to view your data in the app.
- The data is stored in DynamoDB tables.
- 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.