Authorization is probably the most important AppSync API configuration as this defines who can call the API, and as we'll see later what queries, mutations, subscriptions, and fields they can use.
Access control is also defined in the schema using directives, but they don't allow much configuration. You can use the @aws_cognito_user_pools
directive and define the user group, but you can't specify which user pool users need to authenticate to. Similarly, you can add API keys and define in the schema what fields are available using these keys, but creating and rotating them is done on the API level, separate from the schema.
AppSync allows the configuration of a default authorization mode as well as additional authorization providers. Each of these can be one of the supported provider.
With this two-layered approach, you can combine multiple providers, such as IAM and Cognito (a rather useful combination) or even add multiple user pools to a single API.
AppSync supports several providers:
Let's see how each of them works!
A Cognito User Pool is a managed user directory in AWS. It provides a storage for users, defines a flow for authentication, and handles things like passwords, MFA, user groups, and token refresh/revocation. New applications usually use Cognito as it is integrated into the AWS ecosystem and it handles a lot of edge cases out-of-the-box.
Cognito User Pools issue access tokens that services can consume. A logged-in user is identified by its token that it sends with every request made to AppSync.
A successful authentication returns a short-lived AccessToken
:
Access Tokens are short-lived (usually 15 minutes to a few hours), while Refresh Tokens have a long expiration time (months to years). A Refresh Token can request new Access Tokens, so make sure you store it securely.
Then requests to AppSync needs to include this token in the Authorization
header:
Cognito User Pools return access tokens that the consuming services can validate against the user pool. Services that support User Pools (such as AppSync) do this automatically.
Cognito Identity Pools return AWS IAM credentials (Access Key ID and Secret Access Keys), so they provide direct access to the AWS environment. As a result, if you use Identity Pools you need to configure IAM authorization for AppSync.
Usually, you'll want to use User Pools and not Identity Pools.
Configuring a Cognito User Pool for an API allows the use of the @aws_cognito_user_pools
and the @aws_auth
directives (depending on whether there are multiple providers configured or just a single one) that can restrict access to specific groups:
type Query {
# everybody can get themselves
currentUser: User
# only admins can query all users
allUsers: [User]
@aws_cognito_user_pools(cognito_groups: ["admin"])
}
IAM authorization is useful when the caller is an IAM identity: an IAM user or an IAM role. This is usually the case when using the Management Console, or when you want to call the API from a Lambda function or the AWS CLI.
IAM identities can have IAM policies and those policies can give them access to AWS resources. As an AppSync API is an AWS resource, a policy can give access to its queries, mutations, and subscriptions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["appsync:GraphQL"],
"Resource": [
"arn:aws:appsync:<region>:<accountid>:apis/<apiid>/types/Query/fields/allUsers",
]
}
]
}
An IAM policy is necessary to give access to an API, but it might not be sufficient depending on how the API authorization is configured. If you have multiple providers, you also need to add the @aws_iam
directive in the schema:
type Query {
# everybody can get themselves
currentUser: User
# only admins and IAM identities can query all users
allUsers: [User]
@aws_cognito_user_pools(cognito_groups: ["admin"])
@aws_iam
}
Under the hood, calls with IAM credentials (Access Key ID and Secret Access Key) use an AWS signature on the HTTP request.
For example, this is an AppSync query, signed with an IAM role's credentials:
HttpRequest {
method: 'POST',
hostname: 'zb6sucjqnna73ncwc4juglddxi.appsync-api...',
port: undefined,
query: {},
headers: {
host: 'zb6sucjqnna73ncwc4juglddxi.appsync-api...',
'x-amz-date': '20220215T082225Z',
'x-amz-security-token': 'IQoJb3JpZ2luX2VjE...',
'x-amz-content-sha256': 'baf7492d61aaacdcb...',
authorization: 'AWS4-HMAC-SHA256 ... , Signature=21e592...'
},
body: '{"query":"\\nquery MyQuery {...}\\n ", "operationName":"MyQuery"}',
protocol: 'https:',
path: '/graphql'
}
Notice that it has some extra headers: the x-amz-date
, the x-amz-security-token
, and the x-amz-content-sha256
, and it also has a Signature
in the authorization
header. These are results of the signature process, and AWS automatically checks that the request contains all the required elements and that it was signed by an identity with sufficient permissions.
Usually, you don't need to know how the signature process works, and AWS provides tools to sign a request.
The AWS JS SDK v3 provides all the necessary libraries to calculate the signature and sign a request. With it, you can implement Lambda fuctions that call AppSync using their roles' permissions.
First, import the libraries:
import {HttpRequest} from "@aws-sdk/protocol-http";
import {SignatureV4} from "@aws-sdk/signature-v4";
import {defaultProvider}
from "@aws-sdk/credential-provider-node";
import fetch from "node-fetch";
import {URL} from "url";
import pkg from "@aws-crypto/sha256-js";
const {Sha256} = pkg;
// this is the AppSync API URL
const appsyncAPI =
"https://syr5fg5g2navzh2u3emfhhlyze.appsync-api.eu-central-1.amazonaws.com/graphql";
Then create the HTTP request:
const {host, pathname} = new URL(appsyncAPI);
const request = new HttpRequest({
body: JSON.stringify({
// the GraphQL query
query: `
query MyQuery {
test_nothing
}
`,
// operation name
operationName: "MyQuery",
// it also supports variables
}),
headers: {
host,
},
hostname: host,
method: "POST",
path: pathname,
});
Initialize the signer:
const signer = new SignatureV4({
credentials: defaultProvider(),
region: host
.match(/^[^.]+\.appsync-api\.(?<region>[^.]+)\..*$/)
.groups.region,
service: "appsync",
sha256: Sha256
});
Then generate the signed request and send it:
// generate the signed request
const signedRequest = await signer.sign(request);
// send the request
const res = await fetch(
signedRequest.protocol +
"//" +
signedRequest.hostname +
signedRequest.path,
{
body: signedRequest.body,
headers: signedRequest.headers,
method: signedRequest.method,
}
);
// print the result
console.log(await res.json());
If you have users in a directory that supports OpenID Connect then you can configure that with AppSync. For example, Auth0 provides a user directory that is compatible with AppSync.
To configure an Auth0 user directory with AppSync, first create an Application:
Then add its domain for the AppSync API:
This is all the configuration you'll need, now users authenticated on Auth0 can send GraphQL requests to the AppSync API. This requires the OAuth flow to get the ID token first:
Then include that token to the GraphQL requests, just like with Cognito:
With this, a user in the Auth0 domain can log in and send requests:
AppSync resolvers get the information about the signed-in user: