I recently gave a talk at React Amsterdam on running GraphQL at scale , covering some different use cases and techniques that come up in production. One such scenario was related to authorization in a cascading manner using multiple data sources which motivated this article. This is a bit of an advanced subject so if you’re just getting started, I recommend starting with more introductory information here and here . Granting access to data in can be a tricky subject, with multiple strategies and starting to pop up as the technology becomes more widely adopted. gives you powerful techniques to enforce different authorization controls for use cases like: GraphQL emerging best practices GraphQL Completely 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 Fundamentally when implementing access control in any system, some metadata must exist about who or what can access a resource. The classical way of defining this is with Butler Lampson’s where granted permissions are the intersection of rows and columns comprised of resources and actors (which can be users, roles, groups, etc.). When interacting directly with a database this “authorization metadata” many times is a part of that system and the access control is performed at connection time or runtime. In a service architecture fronted by a GraphQL API, a level of indirection exists where database interactions are happening on behalf of the caller. GraphQL also allows authorization to take place at the field level for partial results to be returned on a query. While this may seem daunting at first, in fact you end up with powerful controls which allow you to store authorization data on a resource, in a separate data source, or mix/match for combinations of controls. You can even cascade checks at different levels to meet your unique business needs. Access Control Matrix This article demonstrates how you can use GraphQL techniques for authorization using , however these strategies can be applied to custom GraphQL solutions as well. While AppSync has a first class concept called a “data source”, the generalization can be applied to GraphQL resolvers as the location they use to fetch data. Using the above description on performing access control checks, you can see in the below diagram how this can be done by storing the metadata for authorization on a resource (the “author” column of a database record) and threading identity information of the caller through a request to perform a conditional check when a GraphQL resolver is invoked. Read more . AWS AppSync here While many scenarios for access control can use a single data source, some authorization use cases require the use of multiple data sources. For instance in AppSync you might do this because you want to first perform some logic in a Lambda function before fetching or writing data to a DynamoDB table, or it could be to first perform a lookup such as allowing only people who you are “friends” with to read your data (1:many checks). GraphQL makes this possible by using resolvers on your fields and walking the application graph, fetching data from different data sources and performing authorization checks where appropriate. You can use this technique along with the for many advanced scenarios. For example while fine grained access controls allow you to perform conditional logic inside the resolver, such as user or group checks, nested resolvers and aggregated context allow you to perform this logic using results from the parent object. To demonstrate this we’ll start with a simple use case. built-in fine grained access controls of AWS AppSync Nested resolvers In the , authorization metadata is stored directly on the resource (such as an attribute for an item in a table called “owner”). For demonstrative purposes, suppose these were separate tables and I want to read data in a “Data” table but first run an authorization check against data in an “Auth” table which lists who is the “Owner” of an item in the “Data” table. Only the “Owner” listed in the “Auth” table has rights to read the corresponding information in the “Data” table (the IDs in each table have an implicit association). The table layout is in the below image with some records and pseudocode of how you would want the logical check to work. AWS AppSync fine grained access control examples In this layout, user Nadia would have access to items 1&3 in the “Data” table while Shaggy has access to item 2. To use separate data sources, you can “nest” the data you want to retrieve inside a GraphQL type that gets your authorization metadata from a separate data source and essentially walk the object graph in your query. An example GraphQL schema to perform this nesting is below. type AuthCheckData { id: ID! data: Data } type Data { id: ID! title: String content: String } type Query { getData(id:ID!): AuthCheckData } In the above GraphQL schema the query will invoke a resolver against the and return a type . Then a resolver set on the field will have access to the returned data of it’s parent field to perform authorization checks. A diagram of the flow is below. getData Auth table AuthCheckData data:Data The resolver request template is fairly standard — it actually only needs to run a against the Auth DynamoDB data source with no other arguments: getData GetItem {"version": "2017-02-28", "operation": "GetItem", "key": { "id": $util.dynamodb.toDynamoDBJson($ctx.args.id), } } The response template is just passing through the data: $util.toJson($context.result) Since the whole result is being returned, any of the attributes from the table (in this case ) are returned and available for the child fields to use in a resolver template via the object (aliased as ). Auth Owner $context.source $ctx.source This is where things get interesting. Set a resolver on and again use a but now use the ID from the parent with in the resolver request template: data: Data GetItem, $ctx.source.id { "version" : "2017-02-28", "operation" : "GetItem", "key" : { "id" : { "S" : "${ctx.source.id}" } } } This is important because you’re essentially ensuring that the authorization metadata from the parent check matches the data you’re performing validation against, which just a single argument passed into your GraphQL query. Now you can filter the results in the response template for the resolver to only return values from the Data table if the (available in from the Auth table) is equal to the current user, as seen in the below resolver response template: data: Data Owner $context.source.Owner #if ($context.source.Owner == $context.identity.username) $util.toJson($ctx.result) #else $utils.unauthorized() #end At this point a GraphQL query of or can be run and if Nadia is logged in she’ll see her data. The query would look similar to this: getData(id:1) getData(id:3) query {getData(id:1){data{idtitlecontent}}} If Nadia runs a query of she’ll get an unauthorized message, but if Shaggy runs that query he’ll see the data as he’s listed as the owner in the Auth table. getData(id:2) AWS Lambda Authorizer Suppose you have more complex business logic for authorization, token validation, or you need to interact with a data source that AWS AppSync does not yet support. However, you want to return results from an DynamoDB table if an authorization check passes. You can use the same architecture as before but this time, have the resolver use an AWS Lambda function for the first layer of access to perform authorization and pass the results to your DynamoDB resolver. getData() To set this up use the same schema from earlier, but now add a new AppSync data source of an AWS Lambda function. If you’re not familiar with setting up and using . You’ll need to create the Lambda function first before using it with AppSync, for this example use the below function written in Node.JS: Lambda functions in AWS AppSync take a look at this tutorial first 'use strict'; exports.handler = (event, context, callback) => {console.log(event); let valid = allow(event); switch(event.field) { case "getData": var id = event.arguments.id; if (valid) { callback(null, {id: event.arguments.id}); } else { let result = {}; result.errorMessage = "Error with the authorization"; result.errorType = "AUTHORIZATION\_ERROR"; callback(null, result); } break; case "addData": // Write similar authorization check here callback(null, event.arguments); break; default: callback("Unknown field, unable to resolve" + event.field, null); break; } }; function allow(event) {const allowedKeys = ["abcdef", "ghijkl"];const allowedUsers = ["Nadia", "Caesar"];const allowedIps = ["192.168.0.1"]; if (allowedUsers.includes(event.identity.username)) return true; else if (allowedKeys.includes(event.request.headers.x-api-key)) return true; else if (allowedIps.includes(event.identity.sourceIp)) return true; else return false; } The code above has an function that authorizes requests from a set of Users, API Keys, or IP Addresses. If any of them is a match from then authorization will be allowed. For instance if a client passes in a valid API Key, is in a whitelisted IP address list, or is one of a valid list of users then access will be granted. . If the client calling your GraphQL API doesn’t pass these checks then the Lambda will return an of . This is a fictitious example, but the point is that you can perform your custom authorization logic in your Lambda and cascade the data fetching invocation to other resolvers, which you’ll do next, without needing to put everything in a Lambda function. allow() You can see a full list of identity object threaded into the context object of an AppSync request here errorType AUTHORIZATION_ERROR Change the resolver on to use this AWS Lambda data source. The resolver request template needs to specify the GraphQL field being invoked, and also the identity and arguments: getData(): AuthCheckData {"version" : "2017-02-28","operation": "Invoke","payload": {"field": "getData","identity" : $utils.toJson($context.identity),"arguments": $utils.toJson($context.arguments)}} However, unlike with the previous scenario where you passed down from the parent and did the authorization check in the child, with the Lambda returning the directly you can have your check on the resolver response template of the itself: $context.source.Owner result.errorType getData() #if ($context.result.get("errorType") == "AUTHORIZATION_ERROR")$util.unauthorized()#else$util.toJson($context.result)#end With a pattern like this you’re essentially “wrapping” all of the auth logic in the parent resolver so that the child resolver strictly focuses on data fetching and the separation of concerns is cleaner. Now the resolver request template on the field still does a lookup with the data source on the “Data” table using (since that’s what the Lambda returned in a valid authorization case) but the response template is simply: getData() AuthCheckData:data $ctx.source.id $util.toJson($ctx.result) The nice thing about this pattern is the parent resolver, , essentially “wrapped” the authorization logic and only invoked the child to return data if it passed a validity check. getData() Only let my friends read Another very common use case with authorization is where an individual entity will allow several other entities to access a resource in a 1:MANY manner. In social media applications this might be stated as “only allow Nadia’s friends to see her information”. The typical way that you model this using DynamoDB would be to have a “friendship table” that does the check before accessing the actual record. The friendship table would have a composite key comprised of the user ID and the friend ID. Create a “Friend” table in DynamoDB with a Primary Key of and a Sort Key of . Use strings for both. For convenience, also add a column of so that you can quickly switch of someone is no longer a friend (you could always delete the row too). Add this table as an AWS AppSync data source and edit your schema to have a query type of like below. Username Friend Valid friendGetData(id:ID!, friend:String!) type Query {getData(id: ID!): AuthCheckDatafriendGetData(id: ID!, friend: String!): AuthCheckData} The idea is a user can access a record if they happen to be friends with someone. To keep things simple we will pass in an ID of a record we wish to receive from the same table as before, but first do a friendship check against the new table. The request template for looks like the following: Data Friend friendGetData() {"version" : "2017-02-28","operation" : "GetItem","key" : {"Username" : { "S" : "${ctx.identity.username}" },"Friend" : { "S" : "${ctx.args.friend}" }}} This will return the attribute of from your table if you are a friend with the person. Like the last example, we’ll keep the authorization logic in the top level resolver’s response template to keep responsibilities separate. However unlike the last two cases, the “ ” table doesn’t have a primary key of “ID” so it won’t get automatically passed in the source object. Since we want to use the passed as an argument for a key lookup in the “ table when the child resolver is invoked, we also add this to the context object in the response template: Valid Friend id Data” $util.qr($context.result.put("id", "$context.arguments.id"))#if ($context.result.get("Valid") == "TRUE")$util.toJson($context.result)#else$util.unauthorized()#end Now we can access the as in the other examples through . If you run a query: id $context.source.id query friend {friendGetData(id:1 friend:"Nadia"){data{titlecontent}}} This runs successfully for logged in users whom Nadia is friends with. Mutations All of the examples so far have covered queries as conceptually, I wanted to demonstrate the controls that GraphQL gives you to manipulate your authorization schemes. You can apply these techniques to mutations as well, however there will be a subtle difference in that you might need to perform conditional checks in the request pipeline of your resolver rather than a filter on the response. The way that you control this will be very specific to the data source implementation that is used for your mutation. For example, AWS AppSync supports several data sources including Amazon DynamoDB which supports that can be evaluated by the database engine itself. If you think back to the patterns shown in this article, one showed how you can pass authorization metadata from the parent to the child and make a decision vs. “wrapping” all of the authorization decisions in the parent resolver. If your design is using the first technique then you won’t be able to apply authorization logic on the response of a database operation as the write would have already happened. Instead, you’ll need to use that database engine’s execution criteria along with the metadata passed from the parent resolver. In a DynamoDB resolver with AppSync you would add something similar to the following to your resolver request template: condition expressions "operation" : "UpdateItem","condition" : {"expression" : "contains(Edit,:canEdit)","expressionValues" : {":canEdit" : { "S" : "${context.source.canedit}" }}} The above uses the owner from the parent resolver and at runtime if an attribute of was passed down to the child resolver. . Of course it may be the case that your database implementation doesn’t support these capabilities, isn’t performant for these types of runtime evaluations, or your schema design is simply to separate the authorization logic into the parent in which case the other method can still be used. canedit Several examples can be found here Bonus: Mocking and Testing When building out authorization schemes, it can tricky to mock the scenarios and test out different flows for multiple users or groups to see how the authorization rules will actually run in production. You should look to implement mocking and simulation techniques for authorization with any system which will return data to clients. AWS AppSync provides a few different for this. First, AppSync that allows you to mock the GraphQL request & response context. You can use this to see what the behavior will be with different scenarios and information passed or received in resolvers. For instance if I edit the resolver from the AppSync console, and select the button I can create a mock context object simulating the user as well as any response. Using the Lambda resolver from earlier I’ll pass in the result to simulate an unauthorized request: tools supports a full test and debug flow getData Select test context AUTHORIZATION_ERROR Pressing the button in the console will evaluate the request object, including field and identity information, as well as perform any conditional checks. If the logic results in an unauthorized request that will be printed to the screen: Test After mocking your data, you can also run this “live” from the page of the AppSync console which can . But don’t stop there, the console also allows you to login with a valid user account from Amazon Cognito User Pools to perform a real authorization check: Queries live stream results from Amazon CloudWatch Logs You can use this to test conditional rules against user accounts, groups, claims or other properties of the identity context object. Summary These are just a few examples of using GraphQL along with the use cases and techniques. Security controls can be a complex subject in general, so it’s always best to take a look at all of the options and when possible, start simple and only add more when your business requirements change. existing AWS AppSync authorization Richard Threlkeld ( @undef_obj ) works at AWS Mobile and was part of the teams that launched AWS AppSync and AWS Amplify , All opinions expressed herein are my own.