AppSync — How to allow guest access while limiting authenticated users access to only what they own ?

Bishon Bopanna
8 min readJun 16, 2019

--

AppSync is the GraphQL offering from AWS, which is still in its early days. Like any good wine a good service needs time to mature, even AppSync. What makes AppSync one of the best GraphQL APIs is obviously the backbone behind it — Amazon and its smart group of engineers. Recent add on : Amplify tool chain takes this to the next level and I am enjoying using it. That said there are few obvious use cases which are not available yet off the shelf (promised to be on their trajectory)

Here is the use case :

  1. Allow guest users read only access to certain data
  2. Allow authenticated users to read and write access while ensuring that they have access to only the data they own. Authorization.

Before proceeding, few assumptions : You are familiar with the usage of Amplify and AWS GraphQL. If not, please read-up on these before you proceed.

When you add a GraphQL API to your app through Amplify, the obvious choice for authorization is “Cognito User Pool” or even “API Key”

~/git-source » amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: imball
? Choose an authorization type for the API Amazon Cognito User Pool

At the writing of this blog (June 14th 2019) with choice of “Cognito User Pool” for authorization or even “API Key” there is no way to allow unauthenticated/guest users access to an API, not even for the read/get/list part of the API. This is one of the common scenarios for many apps where you would like for guest users to read the data — say simple search or lookup, while authenticated users to be able to create/update/delete the data they created and also to limit the authenticated users to only read and update the data they own.

Thanks to Nader Dabit and his team, there is an alternative : AWS AppSync — Authenticated & Unauthenticated Users , till a more permanent solution is rolled out. To summarize what is there in the link — use IAM (Identity and Access Management) to control access to the APIs to achieve the above use case. When I tried the same, there were few quirks which became the reason for this article, more on that after the below side note.

When you follow the steps mentioned in AWS AppSync — Authenticated & Unauthenticated Users, there are few crucial touch points from the link — When you add an in-line policy for Auth and UnAuth roles :

  1. For AuthRole — Add all the CRUD APIs — list, create, update and delete (if you intend your authenticated user to be able to do all these of-course). Note — List and Get APIs are under “Query” and the Create, Update and Delete are under “Mutation”.

Example for an “Event” API’s authRole inline policy:

{
"Version": "2012–10–17",
"Statement": [{
"Effect": "Allow",
"Action": [
"appsync:GraphQL"
],
"Resource": [
"arn:aws:appsync:us-east-1:<YOUR_ACCOUNT_ID>:apis/<YOUR_API_ID>/types/Query/fields/listEvents",
"arn:aws:appsync:us-east-1:<YOUR_ACCOUNT_ID>:apis/<YOUR_API_ID>/types/Mutation/fields/createEvent",
"arn:aws:appsync:us-east-1:<YOUR_ACCOUNT_ID>:apis/<YOUR_API_ID>/types/Mutation/fields/updateEvent",
"arn:aws:appsync:us-east-1:<YOUR_ACCOUNT_ID>:apis/<YOUR_API_ID>/types/Mutation/fields/deleteEvent"
]
}]
}

2. For UnAuthRole — Add ONLY the — list APIs as you want your Unauthenticated users to have only read only access to the APIs/data

Example for an “Event” API’s UnAuthRole inline policy:

{

Version”: ”2012– 10– 17 ",“
Statement”: [{

Effect”: ”Allow”,
“Action”: [“appsync: GraphQL”],
“Resource”: [
“arn:aws:appsync:us-east-1:<YOUR_ACCOUNT_ID>:apis/<YOUR_API_ID>/types/Query/fields/listEvents”
]
}]
}

3. <YOUR_ACCOUNT_ID> is found here : https://console.aws.amazon.com/billing/home?#/account

4. <YOUR_API_ID> is found under the settings tab of the GraphQL API that you are working with.

Example :

Now that is out of the way, the next steps — after following the steps in AWS AppSync — Authenticated & Unauthenticated Users you will notice that a guest user is able to read from the API — Yay!, along with any authenticated user too. Along with that you will also observe that any authenticated user is able to read any other authenticated user’s data — hey! is this right! but wait! that is what is wanted! Or is it not ? You need only guest users to read any data while limit your authenticated users to read only their data and _only_ be able to update the records that they created and _not_ any other authenticated users records. Why so ? If not what does the word “Authorization” mean anyway! If that was confusing, scroll back to the requirements that we started off with.

Let us take an example, let us create an Event API with the below SDL (more). Please take note that here the “rules” directive has been used to limit only the owner to create, update and delete on the Mutations while get and list on the Queries. Can I do the same with guest users to say — allow only get and list ? Nope, not at this stage with “Cognito User Pool”/ “API Key”, but does this work with “IAM” as-is as provided in the article — AWS AppSync — Authenticated & Unauthenticated Users, nope, not yet, hence this article, read along.

type Event 
@model
@versioned
@auth(
rules: [
{ allow: owner, ownerField: "owner", mutations: [create, update, delete], queries: [get, list]}
])
{
id: ID!
owner: String
name: String!
etc: String!
}

Once you add the GraphQL API for the above SDL and if you use the create API with two different Authenticated users , say User1 and User2 and use the list API to read the data/record that you created you will see that one authenticated user gets the records created by the other , i.e User1 can see User2's record and vice versa. Also User1 can update the record created by User2 vice versa. With this there is no more Authorization! Reason ? the “owner” field/column in the DynamoDB table for this API has “___xamznone____” as the owner — which not User1 nor User2.

Now this is the value for the owner field for the records created by your User1 and User2. Where did this value come from ? Answer is in the “Resolver” for the create API. Wait! What is a Resolver ? If not sure refer — here. In short, Resolvers are the bridge/connection from the AppSync API to the Data Source/DB wherein you can control the request and response by writing code in Velocity Template.

Let us look into the Resolver for the for the createEvent API.

Resolvers are on the right hand side of Schema menu of your GraphQL API.

Example :

createEvent API Resolver :

## No Static Group Authorization Rules **
## No Dynamic Group Authorization Rules **
## [Start] Owner Authorization Checks **
#set( $isOwnerAuthorized = false )
## Authorization rule: { allow: "owner", ownerField: "owner", identityField: "cognito:username" } **
#set( $allowedOwners0 = $util.defaultIfNull($ctx.args.input.owner, null) )
#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )
#if( $util.isList($allowedOwners0) )
#foreach( $allowedOwner in $allowedOwners0 )
#if( $allowedOwner == $identityValue )
#set( $isOwnerAuthorized = true )
#end
#end
#end
#if( $util.isString($allowedOwners0) )
#if( $allowedOwners0 == $identityValue )
#set( $isOwnerAuthorized = true )
#end
#end
#if( $util.isNull($allowedOwners0) && (! $ctx.args.input.containsKey("owner")) )
$util.qr($ctx.args.input.put("owner", $identityValue))
#set( $isOwnerAuthorized = true )
#end
## [End] Owner Authorization Checks **
## [Start] Throw if unauthorized **
#if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) )
$util.unauthorized()
#end
## [End] Throw if unauthorized **
## [Start] Setting "version" to 1. **
$util.qr($ctx.args.input.put("version", 1))
## [End] Setting "version" to 1. **
## [Start] Prepare DynamoDB PutItem Request. **
$util.qr($context.args.input.put("createdAt", $util.time.nowISO8601()))
$util.qr($context.args.input.put("updatedAt", $util.time.nowISO8601()))
$util.qr($context.args.input.put("__typename", "Event"))
{
"version": "2017–02–28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($util.defaultIfNullOrBlank($ctx.args.input.id, $util.autoId()))
},
"attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id"
}
}
}
## [End] Prepare DynamoDB PutItem Request. **

Important lines from above:

#1 :

#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )

#2 :

$util.qr($ctx.args.input.put("owner", $identityValue))

Now you know from where the value for “owner” is coming from — the identityValue. When we changed the “Default authorization mode” as per AWS AppSync — Authenticated & Unauthenticated Users to “AWS Identity And Access Management (IAM)”, this value is now no longer available from “ctx.identity.claims” and hence defaults to “___xamznone____for all users, and hence for each user the owner value will be ___xamznone____ and hence one user can access and also update other’s data — Authorization gone!

Course correction to get Authorization to work as expected:

Step #1 : Update your the create Resolver’s line setting the identityValue to use the congnitoIdentityId:

Replace :

#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), "___xamznone____")) )

with this :

#set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get("username"), $util.defaultIfNull($ctx.identity.claims.get("cognito:username"), $util.defaultIfNull($ctx.identity.cognitoIdentityId, "___xamznone____"))) )

OR simply to this :

#set( $identityValue = $util.defaultIfNull($ctx.identity.cognitoIdentityId, “___xamznone____”) )

We now will get the identity from the context’s (or ctx) identity object’s cognitoIdentityId. This will be the data provider for the “owner” field, which will be something like this — “us-east-1:3453465–271d-4b2a-5536–456496855sdf” which is the cognitoIdentityId for you authenticated user, say User1. This value will be different for different User.

Now that the create resolver is updated and saved. The list API will not return any data if create a Event record but if you check the DynamoDB table for this API you will see a record created with the owner fields populated with the user’s cognitoIdentityId.

Step #2 : Update the list and get API resolvers so that it returns the data of the user who created it.

Replace :

set( $identityValue = $util.defaultIfNull($ctx.identity.claims.get(“username”), $util.defaultIfNull($ctx.identity.claims.get(“cognito:username”), “___xamznone____”)) )

with this :

#set( $identityValue = $util.defaultIfNull($ctx.identity.cognitoIdentityId, “___xamznone____”) )

Again we are setting the identityValue with cognitoIdentityId

With this the authenticated user will be able only access their own record. That is, User1 will not get User2’s record nor the other way round.

Step #3 : Update the update and delete resolver too to use cognitoIdentityId :

Update and Delete API resolver :

Replace

$util.qr($ownerAuthExpressionValues.put(“:identity0”, $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.claims.get(“username”), $util.defaultIfNull($ctx.identity.claims.get(“cognito:username”), “___xamznone____”)))))

with this :

$util.qr($ownerAuthExpressionValues.put(“:identity0”, $util.dynamodb.toDynamoDB($util.defaultIfNull($ctx.identity.cognitoIdentityId, “___xamznone____”))))

That is it! Guest users now have read only access while authenticated users can read only the data they own and update/delete the data they created and not any other authenticated user’s data.

--

--