Security
Topics
This section describes options for configuring security and data protection for your applications.
There are four ways you can authorize applications to interact with your AWS AppSync GraphQL API. You specify which authorization type you use by specifying one of the following authorization type values in your AWS AppSync API or CLI call:
-
-
API_KEY
-
For using API keys.
-
-
-
AWS_IAM
-
For using AWS Identity and Access Management (IAM) permissions.
-
-
-
OPENID_CONNECT
-
For using your OpenID Connect provider.
-
-
-
AMAZON_COGNITO_USER_POOLS
-
For using an Amazon Cognito user pool.
-
These basic authorization types work for most developers. For more advanced use cases,
you can add additional authorization modes through the console, the CLI, and AWS CloudFormation.
For additional authorization modes, AppSync provides an authorization type that takes
the values listed above (that is, API_KEY
, AWS_IAM
, OPENID_CONNECT
, AMAZON_COGNITO_USER_POOLS
).
When you specify API_KEY
or AWS_IAM
as the main or default authorization type, you can't specify them again as one of
the additional authorization modes. Similarly, you can't duplicate API_KEY
and AWS_IAM
inside the additional authorization modes. You can use multiple Amazon Cognito User
Pools and OpenID Connect providers. However, you can't use duplicate Amazon Cognito
User Pools or OpenID Connect providers between the default authorization mode and
any of the additional authorization modes. You can specify different clients for your
Amazon Cognito User Pool or OpenID Connect provider using the corresponding configuration
regex.
API_KEY Authorization
Unauthenticated APIs require more strict throttling than authenticated APIs. One way to control throttling for unauthenticated GraphQL endpoints is through the use of API keys. An API key is a hard-coded value in your application that is generated by the AWS AppSync service when you create an unauthenticated GraphQL endpoint. You can rotate API keys from the console, from the CLI, or from the AWS AppSync API Reference.
API keys are configurable for up to 365 days, and you can extend an existing expiration date for up to another 365 days from that day. API Keys are recommended for development purposes or use cases where it's safe to expose a public API.
On the client, the API key is specified by the header x-api-key
.
For example, if your API_KEY
is 'ABC123'
, you can send a GraphQL query via curl
as follows:
$ curl -XPOST -H "Content-Type:application/graphql" -H "x-api-key:ABC123" -d '{ "query": "query { movies { id } }" }' http://YOURAPPSYNCENDPOINT/graphql
AWS_IAM Authorization
This authorization type enforces the AWS Signature Version 4 Signing Process on the GraphQL API. You can associate Identity and Access Management (IAM) access policies with this authorization type. Your application can leverage this association by using an access key (which consists of an access key ID and secret access key) or by using short-lived, temporary credentials provided by Amazon Cognito Federated Identities.
If you want a role that has access to perform all data operations:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "appsync:GraphQL" ], "Resource": [ "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/*" ] } ] }
You can find YourGraphQLApiId
from the main API listing page in the AppSync console, directly under the name of
your API. Alternatively you can retrieve it with the CLI: aws appsync list-graphql-apis
If you want to restrict access to just certain GraphQL operations, you can do this
for the root Query
, Mutation
, and Subscription
fields.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "appsync:GraphQL" ], "Resource": [ "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/types/Query/fields/<Field-1>", "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/types/Query/fields/<Field-2>", "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/types/Mutation/fields/<Field-1>", "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/types/Subscription/fields/<Field-1>" ] } ] }
For example, suppose you have the following schema and you want to restrict access to getting all posts:
schema { query: Query mutation: Mutation } type Query { posts:[Post!]! } type Mutation { addPost(id:ID!, title:String!):Post! }
The corresponding IAM policy for a role (that you could attach to an Amazon Cognito identity pool, for example) would look like the following:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "appsync:GraphQL" ], "Resource": [ "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/types/Query/fields/posts" ] } ] }
OPENID_CONNECT Authorization
This authorization type enforces OpenID Connect (OIDC) tokens provided by an OIDC-compliant service. Your application can leverage users and privileges defined by your OIDC provider for controlling access.
An Issuer URL is the only required configuration value that you provide to AWS AppSync
(for example, https://auth.example.com
). This URL must be addressable over HTTPS. AWS AppSync appends /.well-known/openid-configuration
to the issuer URL and locates the OpenID configuration at https://auth.example.com/.well-known/openid-configuration
per the OpenID Connect Discovery specification. It expects to retrieve an RFC5785 compliant JSON document at this URL. This JSON document must contain a jwks_uri
key, which points to the JSON Web Key Set (JWKS) document with the signing keys.
AWS AppSync requires the JWKS to contain JSON fields of alg
, kty
, and kid
.
AWS AppSync supports RS256, RS384, and RS512 as signing algorithms. Tokens issued
by the provider must include the time at which the token was issued (iat
) and may include the time at which it was authenticated (auth_time
). You can provide TTL values for issued time (iatTTL
) and authentication time (authTTL
) in your OpenID Connect configuration for additional validation. If your provider
authorizes multiple applications, you can also provide a regular expression (clientId
) that is used to authorize by client ID.
To validate multiple client IDs use the pipeline operator ("|") which is an "or" in regex. For example, if your OIDC application has four clients with client IDs such as 0A1S2D, 1F4G9H, 1J6L4B, 6GS5MG, to validate for only the first three client ids you would place 1F4G9H|1J6L4B|6GS5MG in the client ID field.
AMAZON_COGNITO_USER_POOLS Authorization
This authorization type enforces OIDC tokens provided by Amazon Cognito User Pools. Your application can leverage the users and groups in your user pools and associate these with GraphQL fields for controlling access.
When using Amazon Cognito User Pools, you can create groups that users belong to. This information is encoded in a JWT token that your application sends to AWS AppSync in an authorization header when sending GraphQL operations. You can use GraphQL directives on the schema to control which groups can invoke which resolvers on a field, thereby giving more controlled access to your customers.
For example, suppose you have the following GraphQL schema:
schema { query: Query mutation: Mutation } type Query { posts:[Post!]! } type Mutation { addPost(id:ID!, title:String!):Post! } ...
If you have two groups in Amazon Cognito User Pools - bloggers and readers - and you want to restrict the readers so that they cannot add new entries, then your schema should look like this:
schema { query: Query mutation: Mutation }
type Query { posts:[Post!]! @aws_auth(cognito_groups: ["Bloggers", "Readers"]) } type Mutation { addPost(id:ID!, title:String!):Post! @aws_auth(cognito_groups: ["Bloggers"]) } ...
Note that you can omit the @aws_auth
directive if you want to default to a specific grant-or-deny strategy on access.
You can specify the grant-or-deny strategy in the user pool configuration when you
create your GraphQL API via the console or via the following CLI command:
$ aws appsync --region us-west-2 create-graphql-api --authentication-type AMAZON_COGNITO_USER_POOLS --name userpoolstest --user-pool-config '{ "userPoolId":"test", "defaultEffect":"ALLOW", "awsRegion":"us-west-2"}'
Using Additional Authorization Modes
When you add additional authorization modes, you can directly configure the authorization
setting at the AWS AppSync GraphQL API level (that is, the authenticationType
field that you can directly configure on the GraphqlApi
object) and it acts as the default on the schema. This means that any type that doesn't
have a specific directive has to pass the API level authorization setting.
At the schema level, you can specify additional authorization modes using directives
on the schema. You can specify authorization modes on individual fields in the schema.
For example, for API_KEY
authorization you would use @aws_api_key
on schema object type definitions/fields. The following directives are supported
on schema fields and object type definitions:
-
@aws_api_key
- To specify the field isAPI_KEY
authorized. -
@aws_iam
- To specify that the field isAWS_IAM
authorized. -
@aws_oidc
- To specify that the field isOPENID_CONNECT
authorized. -
@aws_cognito_user_pools
- To specify that the field isAMAZON_COGNITO_USER_POOLS
authorized.
You can't use the @aws_auth
directive along with additional authorization modes. @aws_auth
works only in the context of AMAZON_COGNITO_USER_POOLS
authorization with no additional authorization modes. However, you can use the @aws_cognito_user_pools
directive in place of the @aws_auth
directive, using the same arguments. The main difference between the two is that
you can specify @aws_cognito_user_pools
on any field and object type definitions.
To understand how the additional authorization modes work and how they can be specified on a schema, let's have a look at the following schema:
schema { query: Query mutation: Mutation } type Query { getPost(id: ID): Post getAllPosts(): [Post] @aws_api_key } type Mutation { addPost( id: ID! author: String! title: String! content: String! url: String! ): Post! } type Post @aws_api_key @aws_iam { id: ID! author: String title: String content: String url: String ups: Int! downs: Int! version: Int! } ...
For this schema, assume that AWS_IAM
is the default authorization type on the AWS AppSync GraphQL API. This means that
fields that don't have a directive are protected using AWS_IAM
. For example, that's the case for the getPost
field on the Query
type. Schema directives enable you to use more than one authorization mode. For example,
you can have API_KEY
configured as an additional authorization mode on the AWS AppSync GraphQL API, and
you can mark a field using the @aws_api_key
directive (for example, getAllPosts
in this example). Directives work at the field level so you need to give API_KEY
access to the Post
type too. You can do this either by marking each field in the Post
type with a directive, or by marking the Post
type with the @aws_api_key
directive.
To further restrict access to fields in the Post
type you can use directives against individual fields in the Post
type as shown following.
For example, you can add a restrictedContent
field to the Post
type and restrict access to it by using the @aws_iam
directive. AWS_IAM
authenticated requests could access restrictedContent
, however, API_KEY
requests wouldn't be able to access it.
type Post @aws_api_key @aws_iam{ id: ID! author: String title: String content: String url: String ups: Int! downs: Int! version: Int! restrictedContent: String! @aws_iam } ...
Fine-Grained Access Control
The preceding information demonstrates how to restrict or grant access to certain GraphQL fields. If you want to set access controls on the data based on certain conditions (for example, based on the user that's making a call and whether the user owns the data) you can use mapping templates in your resolvers. You can also perform more complex business logic, which we describe in Filtering Information.
This section shows how to set access controls on your data using a DynamoDB resolver mapping template.
Before proceeding any further, if you're not familiar with mapping templates in AWS AppSync, you may want to review the Resolver Mapping Template Reference and the Resolver Mapping Template Reference for DynamoDB.
In the following example using DynamoDB, suppose you're using the preceding blog post schema, and only users that created a post are allowed to edit it. The evaluation process would be for the user to gain credentials in their application, using Amazon Cognito User Pools for example, and then pass these credentials as part of a GraphQL operation. The mapping template will then substitute a value from the credentials (like the username)in a conditional statement which will then be compared to a value in your database.

To add this functionality, add a GraphQL field of editPost
as follows:
schema { query: Query mutation: Mutation } type Query { posts:[Post!]! } type Mutation { editPost(id:ID!, title:String, content:String):Post addPost(id:ID!, title:String!):Post! } ...
The resolver mapping template for editPost
(shown in an example at the end of this section) needs to perform a logical check
against your data store to allow only the user that created a post to edit it. Since
this is an edit operation, it corresponds to an UpdateItem
in DynamoDB. You can perform a conditional check before performing this action, using
context passed through for user identity validation. This is stored in an Identity
object that has the following values:
{ "accountId" : "12321434323", "cognitoIdentityPoolId" : "", "cognitoIdentityId" : "", "sourceIP" : "", "caller" : "ThisistheprincipalARN", "username" : "username", "userArn" : "Sameasabove" }
To use this object in a DynamoDBUpdateItem
call, you need to store the user identity information in the table for comparison.
First, your addPost
mutation needs to store the creator. Second, your editPost
mutation needs to perform the conditional check before updating.
Here is an example of the request mapping template for addPost
that stores the user identity as an Author
column:
{ "version" : "2017-02-28", "operation" : "PutItem", "key" : { "postId" : { "S" : "${context.arguments.id}" } }, "attributeValues" : { "Author" : {"S" : "${context.identity.username}"} #foreach( $entry in $context.arguments.entrySet() ) #if( $entry.key != "id" ) ,"${entry.key}" : { "S" : "${entry.value}" } #end #end }, "condition" : { "expression" : "attribute_not_exists(postId)" } }
Note that the Author
attribute is populated from the Identity
object, which came from the application.
Finally, here is an example of the request mapping template for editPost
, which only updates the content of the blog post if the request comes from the user
that created the post:
{ "version" : "2017-02-28", "operation" : "PutItem", "key" : { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), }, "attributeValues" : $util.dynamodb.toMapValuesJson($ctx.args), "condition" : { "expression" : "contains(#author,:expectedOwner)", "expressionNames" : { "#author" : "Author" }, "expressionValues" : { ":expectedOwner" : { "S" : "${context.identity.username}" } } } }
This example uses a PutItem
that overwrites all values rather than an UpdateItem
, which would be a bit more verbose in an example, but the same concept applies on
the condition
statement block.
Filtering Information
There may be cases where you cannot control the response from your data source, but you don't want to send unnecessary information to clients on a successful write or read to the data source. In these cases, you can filter information by using a response mapping template.
For example, suppose you don't have an appropriate index on your blog post DynamoDB
table (such as an index on Author
). You could run a GetItem
query with the following mapping template:
{ "version" : "2017-02-28", "operation" : "GetItem", "key" : { "postId" : { "S" : "${context.arguments.id}" } } }
This returns all the values responses, even if the caller isn't the author who created the post. To prevent this from happening, you can perform the access check on the response mapping template in this case as follows:
{ #if($context.result["Author"] == "$context.identity.username") $utils.toJson($context.result); #end }
If the caller doesn't match this check, only a null response is returned.
Data source access
AWS AppSync communicates with data sources using Identity and Access Management (IAM) roles and access policies. If you are using an existing role, a Trust Policy needs to be added in order for AWS AppSync to assume the role. The trust relationship will look like below:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "appsync.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
It's important to scope down the access policy on the role to only have permissions to act on the minimal set of resources necessary. When using the AppSync console to create a data source and create a role, this is done automatically for you. However when using a built in sample template from the IAM console to create a role outside of the AWS AppSync console the permissions will not be automatically scoped down on a resource and you should perform this action before moving your application to production.