Tackling User Authorization in GraphQL with AWS AppSync

Written by dabit3 | Published 2018/04/10
Tech Story Tags: graphql | aws-appsync | javascript | reactjs | react

TLDRvia the TL;DR App

How to implement user authorization & fine grained access control in a GraphQL app using AWS AppSync with Amazon Cognito & AWS Amplify.

If this is your first time using AWS AppSync, I would probably recommend that you check out this tutorial before following along here.

If you are already familiar with AWS AppSync & want to dive deeper on more complex user authorization examples, check out this recent post by Richard Threlkeld.

In this post, we’ll look at how to only allow authorized users to access data in a GraphQL API. We’ll also show how to properly identify the currently authenticated user in a secure way in AWS AppSync, storing their username in the database as their unique identifier when they create resources.

Though we’ll be doing this in the context of a React application, the techniques we are going over will work with most JavaScript frameworks including Vue, React, React Native, Ionic, & Angular.

The flow that we will be working with looks like this:

  1. As a user, we log in to the application and receive an identity token.
  2. We invoke a GraphQL query or mutation from the client application, passing the user identity token along with the request in an authorization header (the identity automatically passed along by the AWS AppSync client).
  3. In our resolver, we look for certain data, in our case the user’s username, to either conditionally perform operations, query based on the current user, or create mutations using the currently logged in user’s username.
  4. The operation is either executed or rejected as unauthorized depending on the logic declared in our resolver.

The data flow for a mutation could look something like this:

  1. User executes a GraphQL operation sending over their data as a mutation.
  2. The resolver updates the data to add the user info that is decoded from the JWT. The JWT is sent in the authorization header & is available in the resolver.
  3. Data is stored in the database along with user information.

In this example we can now query based on the author index.

Getting Started

The tools that we will be using to accomplish this are the AWS Amplify CLI to create the authentication service & the AWS Amplify JavaScript Client for client authentication as well as for the GraphQL client.

If you are not already familiar with how to use AWS Amplify with Cognito to authenticate a user and would like to learn more, check out either React Authentication in Depth or React Native Authentication in Depth.

To get started, clone the boilerplate we will be using in this example:

git clone https://github.com/dabit3/appsync-react-native-with-user-authorization

Then, cd into the directory & install the dependencies using yarn or npm:

cd appsync-react-native-with-user-authorization

yarn || npm i

Now that the dependencies are installed, we will use the AWS Amplify CLI to initialize a new project.

First, install the AWS Amplify CLI if you do not already have it installed:

npm i -g @aws-amplify/cli

Next, configure the cli with your correct credentials:

If this is your first time using AWS, check out this video to see how to get these credentials and set up the CLI.

amplify configure

Now we can create a new project:

amplify init

You’ll be prompted with a few configuration options, feel free to accept the defaults to all of them or choose a custom project name when given the option.

Next we will add user-signin capabilities to the app with Amazon Cognito:

amplify add auth

Then push the updated config to the AWS console

amplify push

Now, you should be able to visit the console and view the new service. Go to https://console.aws.amazon.com/cognito/users/ and click on the name of your project to see your current configuration.

Creating the AWS AppSync API

Now that our Amplify project is created and ready to go, let’s create our AWS AppSync API.

First, go to the AWS AppSync console by visiting https://console.aws.amazon.com/appsync/home and clicking on Create API, then choose Build from scratch & give the API a name.

Creating the Schema

Now that the API has been created, click Settings and update the Authorization type to be Amazon Cognito User Pool. In the User Pool configuration, choose the user pool that was created when we created our AWS Amplify project using the CLI along with your region, and set the default action to Allow.

Next, create the following schema and click Save:

type City {id: ID!name: String!country: String!author: String}

type Query {fetchCity(id: ID): City}

Note that author is the only field not required.

Provisioning Resources

Next, click the Create Resources button.

In this screen, choose City as the type, and create an additional index with an Index name of author-index and a primary key of author. Then scroll to the bottom and click Create.

Updating the resolvers

Next, we’ll update a couple of resolvers. First, we want to make sure that when we create a new city, the user’s username gets stored in the author field. This username data is available as part of the user identity token passed along with the request in an authorization header, and we can access this in our resolver as the identity in the context.identity field available in the resolver.

Identifying the currently logged in user in a mutation

In the resolver field under Mutation Data Types in the dashboard click on the resolver for createCity:

Update the createCity request mapping template to the following:

#set($attribs = $util.dynamodb.toMapValues($ctx.args.input))#set($attribs.author = $util.dynamodb.toDynamoDB($ctx.identity.username)){"version": "2017-02-28","operation": "PutItem","key": {"id": $util.dynamodb.toDynamoDBJson($util.autoId()),},"attributeValues": $util.toJson($attribs),"condition": {"expression": "attribute_not_exists(#id)","expressionNames": {"#id": "id",},},}

There are a couple of things to note:

  1. In the first line of code we are creating a new map / object called $attribs and adding the existing values coming in as $ctx.args.input
  2. In the second line of code we are adding another field to the object called author with the value of $ctx.identity.username. $ctx.identity is the identity information coming via the user identity token.
  3. In the attributeValues field, we are passing in the new $attribs object we just created.

Now, when we create a new city, the user’s identity will automatically be stored as another field in the DynamoDB table.

Allowing read access to only the item author

Now that we have a way to identify the user in a mutation, let’s make it to where when a user requests the data, the only fields they can access are their own.

To add this functionality using our existing setup, we only need to do one thing: update the listCities resolver to query only for the data created by the currently logged in user.

DynamoDB allows you to perform Query operations directly on an index. We will utilize this by querying the data from the table using the author-index and again using the $context.identity.username to identify the user.

Update the listCities request mapping template to the following:

{"version" : "2017-02-28","operation" : "Query","index" : "author-index","query" : {"expression": "author = :author","expressionValues" : {":author" : {"S": "${ctx.identity.username}"}}},"nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.after, null)),}

A couple of things changed here:

  1. We updated the operation to be a Query vs a Scan. The difference is that a scan operation scans the entire table whereas a query operation searches only primary key attribute values and supports a subset of comparison operators on key attribute values (in our case author) to refine the search process.
  2. We added an index field specifying the index in which we would like to query
  3. We added another new field, query, defining the value we would like to use as the query value (in our case, author).

Now, the API is complete and we can begin testing it out.

Testing the app

Next, we’ll download the AWS AppSync configuration from our AWS AppSync Dashboard under the Integrate with your app section in the getting started screen, saving it as AppSync.js in our root folder.

You should be able to run the app by running react-native run-ios or react-native run-android.

From the opening screen, choose “Sign Up” and create a new user. Confirm the new user with 2 factor authentication (Make sure to add +1 or your country code when you input your phone number).

Once you’ve signed up, sign in, click on Add City, and create a new city:

Once you create a city, you should be able to click on the Cities tab to view this new city.

Now, let’s go back into the AWS AppSync dashboard. Click on Data Sources, and the table name. This will take you to DynamoDB.

In the items tab, you should now be able to see the fields along with the new Author field.

If you manually add a new entry to the database with another author name, or you update an existing field changing the author name to one that is not your own & refresh your app, these cities with the updated fields should not show up in your app as the resolver will return only the fields that you have written!

Conclusion

When building a real world app there are many important and complex things that need to be taken into consideration, one of the most important being a real world scalable & easy to implement user authorization story.

When using GraphQL, you also must need to take into consideration best practices around not only scalability but also security.

GraphQL gives you the power to enforce different authorization controls for use cases like:

  • A public API
  • Private and Public access to sections of an API
  • Private and Public records, checked at runtime on fields
  • One or more users can write/read to a record(s)
  • One or more groups can write/read to a record(s)
  • Everyone can read but only record creators can edit or delete

One of the most compelling things about AWS AppSync is its powerful built-in user authorization features that allow all of these GraphQL user authorization use cases to be handled out of the box.

My Name is Nader Dabit . I am a Developer Advocate at AWS Mobile working with projects like AWS AppSync and AWS Amplify, and the founder of React Native Training.

If you enjoyed this article, please clap n number of times and share it! Thanks for your time.

Images courtesy of Amazon Web Services, Inc


Published by HackerNoon on 2018/04/10