AWSserverless

AppSync – first jumps in at the deep end – part 4

By 05/24/2021No Comments

Last time we met I was analyzing the “single- vs multi table” Dynamodb data modeling in projects where the AWS AppSync is being leveraged.

TL; TR

This time I will walk you through the security aspects of AWS AppSync. You will see how easily developers can secure the data they’re exposing to the users with just a few graphql schemas tampering steps. When I talk with our clients, I enjoy the moments spent on listening to issues they’re struggling with. Essentially, it’s a great starting point for new ideas. But keep in mind you won’t usually get those right away.

I remember a case where a client wanted to achieve flexibility while returning the data to customers via AWS API Gateway. Basically, he wanted to select parts of the data, either public or private, easily. A quick example: “the response from a GET would return a public data from a Dynamodb, but some particular fields should be restricted to only authenticated users by an OpenID Connect (OIDC) identity provider”. It was doable, of course, but having worked with AWS Appsync before, I realized that there was a much easier method (at least for now).

Security via graphql schema

Using graphql on AppSync provides a powerful and easy way to avoid a single global authorization mode for an API. In your schema, you’re allowed to define different authorization types for its different parts.

Let’s get back to the example with our app backed by a GraphQL API with a particular data type defined in the schema as follows:

type Device{
 id: ID!
 d_status: DeviceStatusEnum
 version: String
 startDateTime: String
 users: [User]
 places: [Place]
 serialNumber: String
 description: String
 deviceGroupId: ID
 actionsHistory: [RemoteActions]
 createdAt: AWSDateTime!
 updatedAt: AWSDateTime!
}

For the sake of the example let’s make the whole type available via API key (there’s no concept of identity so don’t use that in production = good for prototypes) but set the serialNumber as available only for a user authorized by AWS Cognito.

Those of you who have been using API Gateway in their products should see a solution looming on the horizon. We can create two separated APIs with different authorization modes in – one with API key and the other with Cognito User Pools and a serialNumber key/value response. Not exactly a convenient way, but certainly doable. I’ve even heard about custom authorizers coded to support simultaneously multiple authorization methods. Now, let’s see how easy it can be done with our AWS AppSync.

Authorization options in AWS Appsync schema?

 The schema offers 4 methods: @aws_api_key; @aws_iam – the request must be signed with AWS Signature Version 4; @aws_oidc – a nice way if you need to integrate with a pre-existing identity provider and don’t want to do federation; @aws_cognito_user_pools which you can use simultaneously for either Mutations, Queries or Types. So, in our example we would get something like this:

type Device @aws_api_key @aws_cognito_user_pools{
 id: ID!
 d_status: DeviceStatusEnum
 version: String
 startDateTime: String
 users: [User]
 places: [Place]
 serialNumber: String @aws_cognito_user_pools
 description: String
 deviceGroupId: ID
 actionsHistory: [RemoteActions]
 createdAt: AWSDateTime!
 updatedAt: AWSDateTime!
}

We also need a query to request the data with:

type Query @aws_api_key @aws_cognito_user_pools{
 getDevice(id: ID!): Device
}

Of course, to make it all happen we have to launch authorization modes. And here’s where our Serverless Framework comes in handy. All we need is few lines of code (a snippet from a serverless.yml) and we’re ready to go:

custom:
 appSync:
   name: ${self:custom.app}-${self:service}-${self:custom.stage}
   authenticationType: API_KEY
   apiKeys:
     - name:  iotkey # name of the api key
       description: API key for test
       expiresAfter: 90d # api key life time
   additionalAuthenticationProviders:
     - authenticationType: AMAZON_COGNITO_USER_POOLS
       userPoolConfig:
         awsRegion: eu-central-1
         userPoolId: eu-central-1_HmofEI99A
   mappingTemplates:
     - dataSource: Devices
       type: Query
       kind: UNIT # (default, not required) or PIPELINE (required for pipeline resolvers)
       field: getDevice


As you can see, we defined a default global authorization mode and a secondary mode (multiple allowed) that will secure API and become useful during a variety of tests.

 Come on, let’s send some requests

 Whenever I code, Visual Studio Code is my toolbox. So, let me walk you through the queries invoked from its plugin called REST Client.

Initially, I’ll be sending POST requests to my Appsync API endpoint with API KEY. Then, it will be changed into Cognito authorization in which an access token will be obtained for the user. Finally, I will call the API method with the token set to the request’s Authorization header.

I set value for a header x-api-key as a variable and invoke a graphql query which, in simple terms, walks through a Unit resolver on a field named getDevice(id:ID!) that returns a Device type :

Then, Appsync returns a still valid response with partial data which already walked through a response mapping template formatted into:

{
        "data": {
                "GRAPHQL_QUERY_NAME":      {
                        JSON_ITEMS_I_SELECTED_TO_BE_RETURNED
                }
        }
}

The most interesting part is a serialNumber item returned with the value null and the error part. We will take a closer look at that right now.

This particular error is caused by a utility called $util.unauthorized() which is one of AWS AppSync utility sets leveraged within a GraphQL resolvers to simplify interactions with data sources. As you might have noticed above in the path part, the mentioned utility throws Unauthorized for the field is resolved. In our case, it’s the serialNumber field (returned as null).

Here’s a snippet of my Query.getDevice.response.vtl representing part of a response mapping template file. As you can see, $util.unauthorized() is being called if any of check conditions is true:

## [Start] Throw if unauthorized **
 #if( !($isStaticGroupAuthorized == true || $isDynamicGroupAuthorized == true || $isOwnerAuthorized == true) )
   $util.unauthorized()
 #end
 ## [End] Throw if unauthorized **

Let’s call an authorized request

 First of all, an access token has to be obtained. Once again, we use the VSC plugin. Cognito User Pool API is being called with a metadata file (containing necessary params needed for getting the tokens from Cognito) as a payload:

{
   "AuthParameters": {
       "USERNAME": "<email>",
       "PASSWORD": "<password>"
   },
   "AuthFlow": "USER_PASSWORD_AUTH",
   "ClientId": "<cognito user pool id>"
}

NOTE: The user pool access token contains claims about the authenticated user, a list of the user’s groups, and a list of scopes. The purpose of the access token is to authorize API operations in the context of the user in the user pool.

A response from a Cognito Pool with the AccessToken includes:

“AuthenticationResult”: { “AccessToken”: “…”, “ExpiresIn”: 3600, “IdToken”: “…”, “RefreshToken”: “…”, “TokenType”: “Bearer” }, “ChallengeParameters”: {} }

Followed by the invocation of the AppSync API service passing the Authorization HTTP header with the value Bearer <AccessToken> (this {{ token }} value) :

Finally, the full data has been returned successfully.

Summary

 AWS Appsync gives us a huge flexibility in applications development with GraphQl support. As you’ve seen for yourself, the authorization methods implementation via schema and velocity templates (written VTL) is much easier than using API Gateway. But nothing is for granted. Not every application is ready for graphql or has a technological argument for using it. Anyway, if you’re considering a new application or think about extending the one you already have in AWS, you definitely should consider using AWS Appsync. At the end of the day, it might save you a lot of time which in the contemporary race toward faster innovations is worth more than gold.