Multiple Authorization methods in a single GraphQL API with AWS AppSync: Security at the Data Definition Level
AuthN/Z can be complex depending on the use case. It’s important to provide access flexibility to different applications, maybe all data should be public or maybe only parts of the data should be public or maybe we need strict fine grained access control. It’s all about flexibility and the security requirements for your next app.
If you’re using GraphQL, wouldn’t it be great if you could easily define different authorization modes for you application data at the type, field or operation level directly in your schema instead of having a single global authorization mode for an API? What if you could have different types of authorization providers for different parts of your schema? For instance, maybe I need to allow users with an API Key to access some fields and users authenticated by an OpenID Connect (OIDC) identity provider to access the whole data set.
Let’s use an example, I have a very simple notes app backed by a GraphQL API and the data is defined in the schema as a single type:
type Notes {
userid: ID!
noteid: ID!
note: String
email: String
}
Users can create notes and add their e-mail address when they create their notes. I want notes to be public however I don’t want the e-mail address of the author to be available unless I’m authenticated and authorized by my trusted user management system or directory.
In order to solve that problem before with AWS AppSync, one option would be having 2 APIs as the authorization modes were global to the API. I’d need an API with API Keys for public access without the email
field in the Notes
type and a duplicated API with maybe OIDC or Cognito User Pools authorization with the email
field for private access.
Let’s imagine I have my GraphQL API on AppSync with the Notes
type above defined on my schema and the API is configured to authorize access based on API Keys only. If I query my Notes
type I’ll get the whole the data set:
Well, this is not good enough. I don’t want users to see the author’s e-mail address unless they are signed-in and authenticated. How can we get this implemented without duplicating the API?
Now there’s a much easier way, you can use multiple authorization modes in your AppSync GraphQL API:
The API has a default global authorization mode, in this case API Key, however you can add more authorization modes so multiple different authorization providers can secure your GraphQL API calls.
In order to get my API configured so only users from an user directory such as Cognito User Pools can access the author’s e-mail details and all other users can access the rest of the data, I just need to use a couple of schema directives such as:
@aws_api_key
— API_KEY for authorization@aws_iam
— AWS_IAM for authorization@aws_oidc
— OPENID_CONNECT for authorization@aws_cognito_user_pools
— AMAZON_COGNITO_USER_POOLS for authorization
Back to my Notes
type I add the directives I need accordingly:
type Notes @aws_api_key @aws_cognito_user_pools{
userid: ID!
noteid: ID!
note: String
email: String @aws_cognito_user_pools
}
I am using DynamoDB with userId
as partition key and noteId
as sort key to store my data, I need both authorization methods to access the IDs so resolvers can query the table. However I’m explicitly defining that only Cognito User Pools can access my email
field.
We’re not quite done yet. Let’s take a look at how I query my notes:
type NotesConnection {
items: [Notes]
nextToken: String
}type Query {
listNotes(filter: TableNotesFilterInput, limit: Int, nextToken: String): NotesConnection
}
Remember the default authorization mode in my API is to use an API Key. I’m using a listNotes
query to get my data which returns a NotesConnection
type with a list of Notes
. Both the query operation and the NotesConnection
type are only accessible via API Key, the default API authorization mode. I need to make some changes:
type NotesConnection @aws_api_key @aws_cognito_user_pools{
items: [Notes]
nextToken: String
}type Query {
listNotes(filter: TableNotesFilterInput, limit: Int, nextToken: String): NotesConnection @aws_api_key @aws_cognito_user_pools
}
Now I’m allowing all the authorization modes I need to issue the query as well as return the connection type.
Let’s try our query again using an API Key:
Notice I still get a valid response with partial data. I can only access what is defined for my authorization mode, which means the e-mail address is not available if I’m connecting with an API Key. I also get a descriptive error:
{
"path": [
"listNotes",
"items",
0,
"email"
],
"data": null,
"errorType": "Unauthorized",
"errorInfo": null,
"locations": [
{
"line": 22,
"column": 7,
"sourceName": null
}
],
"message": "Not Authorized to access email on type Notes"
}
Next let’s authenticate with a valid user from my Cognito User Pools:
And try the query again:
What if I try another authorization mode? If you recall, I defined IAM as an additional provider. Since I haven’t added any schema directive to authorize IAM calls to my Notes
type, this is what happens :
Mission accomplished! With multiple authorization modes on AWS AppSync I have the flexibility I need in my API to secure data at a field, type or operation level in my GraphQL schema to fit my app requirements, allowing me to easily setup security at the data definition level.