Tuesday, 4 December, 2018 UTC


Summary

In my previous post, "Creating a GraphQL API with AWS", we walked through & learned how to create an AWS AppSync GraphQL API using the AWS Amplify library.
Right now, the configuration for the API is set to API Key. This means that any client that has the correct API Key sent as a header with the http request will be able to access the API to perform operations against it.

AWS AppSync Authentication Types

We would like to update the API so that only authenticated users can access it. We'd also like to add fine-grained access conrol so we can control access to certain data based on the signed in user. With AWS AppSync, we can either use Amazon Cognito User Pools or OpenID Connect to accomplish this.
OpenID Connect (OIDC) Authentication type enforces OIDC tokens provided by an OIDC-compliant service. Your application can leverage users and privileges defined by your OIDC provider for controlling access. This allows you to bring your own or use an existing identity provider to authenticate against your AppSync API.
Amazon Cognito User Pools Authentication type enforces OIDC tokens provided by the user pool being used. Your application can leverage the users and groups in your user pools and associate these with GraphQL fields for controlling access.
Adding the Authentication Provider with AWS Amplify
With the AWS Amplify CLI we can create a new authentication service & also update the authentication configuration of the AWS AppSync API we have already created.
To do so, we'll open the project where we left off in the previous tutorial & run the following command to create the authentication service:
amplify add auth
  • Do you want to use the default authentication and security configuration? Y
Choosing the default authentication & security configuration as we have done above will automatically set up some basica configuration around the authentication service, including email MFA for signing up, & requiring username & password for signing in.
Next, we'll run the following command to create the backend AWS resources in our account:
amplify push
Now, the authentication service is in our account & we can begin logging in!
To try this out, let's use the AWS Amplify React library to our project. This library contains multiple React specific functionality & UI components that we can use to quickly get up & running with useful features:
npm install aws-amplify-react
Now, we can use a React Higher Order Component (HOC) that is made available through the library to add an authentication flow in just a few lines of code:
// import HOC
import { withAuthenticator } from 'aws-amplify-react'

// this stays the same
class App extends Component {}

// update default export
export default withAuthenticator(App, { includeGreetings: true })
If we run the app, we should see a sign-in / sign-up form now protecting the application:
If we sign up & then sign In, we should then be able to view the main application.
If you would like more control over the configuration & design of the withAuthenticator HOC, you can view the documentation here to see what available props can be passed in order to customize it. If you would like full control over the design & configuration of your sign in screen, use the Auth class which has over 30 available methods that allow for full control over your authentication flow.
Updating AWS AppSync to use the new Authentication provider
Now that we have authentication set up we need to update the authentication type of our AWS AppSync API. Right now it's set to API Key, we need to update it to use the new Amazon Cognito User Pool configuration we just created.
This is easy to do, we only need to run one command to update this configuration:
amplify configure api
You'll be prompted for the floowing:
  • Please select from one of the below mentioned services: GraphQL
  • Choose an authorization type for the API: Amazon Cognito User Pool
Once this is completed, we'll run amplify push to update the resources in our account:
amplify push
Now, our API will only be available to signed in users & not accessible via regular API calls from unauthenticated users.
To test mutations & queries from the AppSync console query editor you will need to specify an app client & log in using that app client. To create an app client, go to the cognito user pool dashboard in the correct region, click on your user pool, & then click on App clients in the left hand menu to create a new app client. Make sure that Generate client secret is unchecked when creating the new App client.
Adding Fine-grained Access Control
Now that we have an authenticated GraphQL API, we may want to start implementing some authorization controls on the API. Typically, you'll have resources that you only want made available to certain users or certain groups.
Let's update our existing API to only allow the listRestaurants query to return the data of the logged in user.
To get this working, we need to do three things:
  1. In the request mapping template for the createRestaurant mutation, we will update the data being passed in to the mutation to include information about the user creating the mutation. We'll add another field in the table (userId) & populate the field with the user id of the user that is triggering the mutation. We'll accomplish this by updating the input field with an additional value that we'll get from the user's identity using the $context.identity value available in the request mapping template.
  2. We'll create a userId global secondary index in our table so that we can query only based on that index. This means that we can request data only if the index value matches the index that we are querying for. (To learn more, click here).
  3. Now we need a way to query for the data based on the user. In the response mapping mapping template, we'll use the secondary index to query only for the data that the user making the request created. We'll read the user's unique id in the request mapping template & query based on the value. We'll get the user's identity by accessing the $context.identity available in the mapping template.
1.Updating the request mapping teamplate for createRestaurant
We'll first update the request mapping template for the createRestaurant mutation. To access this mapping template, go to the AWS AppSync console & open your API. In the left hand menu, click on Schema.
While in the Schema view, you will see the resolvers listed on the right hand side. Scroll (or filter) to mutations & click the resolver for the createRestaurant mutation.
In this view, you'll see both the request mapping templae & the response mapping template for this mutation. We'll update the request mapping template to add a new userId field to the input field (context.args.input). To do so, we can add this line & save the :
$util.qr($context.args.input.put("userId", $context.identity.sub))
To see a gist of the new request mapping template, click here.
2. Adding the userId Global Secondary Index to the DynamoDB Table
Now when we create a new item, the user id of the user will also be stored along with the other item information.
We now would like to query the table based on this new userId field. To do so, we'll create a secondary index on the table so that we can query this index based on the user id.
To add a secondary index, we need to open the DynamoDB table for the API. Click on Data Sources in the left menu & then click on the link for the DynamoDB table. Next, click on the Indexes tab & click Create index. Set the Partition key as userId & the read / write capacity units to 1 & click Create index (it may take up to 5 minutes for index to be created).
3. Querying for data based on identity
Now that the secondary index is created & the user's identity is being stored in the database, we can query based on the user's identity. To do so, we'll update the request mapping template for the listRestaurants query resolver.
To update the resolver, click on Schema in the left menu to view the Schema & resolvers. In the resolvers list, scroll to (or filter for) query & click on the listRestaurants resolver. Update the request mapping template of the resolver to the following & click Save Resolver:
#set( $limit = $util.defaultIfNull($context.args.limit, 10) )

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "limit": $limit,
    "index": "userId-index",
    "query" : {
        "expression": "userId = :userId",
        "expressionValues" : {
            ":userId" : $util.dynamodb.toDynamoDBJson($ctx.identity.sub)
        }
    },
    "nextToken":   #if( $context.args.nextToken )
      "$context.args.nextToken"
     #else
       null
    #end
}
In the updated request mapping template we are performing a query based on an index instead of a scan. We're querying the userId index & getting the user's unique id from the context.identity.sub value which is automatically made available from AppSync.
With this updated mapping template, when we call the listRestaurants query we'll only get the data for the user that is currently signed in.
Conclusion
While we've just scratched the surface of how you can implement authorization in an AWS AppSync GraphQL API, we've covered some very important fundamental concepts that can be expanded upon to handle many authorization & fine-grained access control use-cases.
If you'd like to learn more, take a deep-dive into the AWS AppSync Authorization Use Cases documentation here.