Using GraphQL Apollo client with AWS API Gateway secured by Cognito Identity and Userpool

At Orchestrated, we have a habit of poking at new technologies, having passionate discussions about how we develop (test coverage: it’s a love-hate relationship) and look wise while sipping far too much coffee.

Sometimes, we end up using tech which really kicks you under your belly. Some of our new products use ReactJS as frontend hosted in AWS S3 exposed via AWS Cloudfront. We use graph database as datastore and state change handled through, event sourcing, which people tell us is great.

So — I convinced the team to try GraphQL as API layer over REST. I had strong arguments for maintainability, contracts, etc. New tech feels like shiny new Credit Card, at first it’s a great experience — the debt and fees come later :( . Here’s what we learned!


Problem Statement

We wanted to get started quickly and at low cost, so our GraphQL server is hosted in AWS as a Lambda function. The GraphQL lambda is exposed via the AWS API Gateway. To get a quick layer of security, we used AWS Cognito Userpool with the API Gateway. We used ApolloClient in the frontend to interact with GraphQL.

Then the debt was called in. It was hard for us to figure out how to call AWS API gateway when secured by Cognito Userpool or by Cognito Identity IAM role. Cognito User pool secured API Gateway requires Authorization header and the Cognito IAM secured API Gateway requires AWS Signed request header. After much hard work, we found a solution to these problem that’s easy to implement which we thought good to share in this article.

Typically, you initialise an Apollo Client in React as below which doesn’t cater for additional Headers. So how to add these headers:

const client = new ApolloClient({
link: new HttpLink({ uri: "https://localhost:3000/graphql"}),
cache: new InMemoryCache()
});
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);

Solution:

Crux of the solution is to leverage amazing work done by Apollo GraphQL team. We are going to create an extension for Apollo client link and replace fetch library with our custom fetch. Custom fetch will look like this :

const awsGraphqlFetch = (uri, options) => {
// Do Custom stuff here, feels better breaking stuff
  options.headers["gym"] = "stress";
return fetch(uri, options);
};
const client = new ApolloClient({
link: new HttpLink({
uri: "https://localhost:3000/graphql",
fetch: awsGraphqlFetch
}),
cache: new InMemoryCache()
});

Solution: Integrate with AWS Userpool (Easy) :

AWS Userpool is your regular identity provider just like Google or Facebook which gives you a JWT token ( can use access token or Id token with AWS Userpool) on Authentication. We need to populate this token in Authorization header whenever we are requesting resource against endpoints secured by AWS Userpool.

Below snippet, fetches JWT token (getJwtToken — its our custom function to get token stored in our local storage. you have to write your own) and populate every request to graphql server via Apollo Client.

const awsGraphqlFetch = (uri, options) => {
const token = getJwtToken()
options.headers["Authorization"] = token;
return fetch(uri, options);
};
const client = new ApolloClient({
link: new HttpLink({
uri: "https://XXXXXXX.execute-api.us-east-1.amazonaws.com/production/graphql",
fetch: awsGraphqlFetch
}),
cache: new InMemoryCache()
});

Solution: Integrate with AWS IAM Identity (Trickier) :

AWS Cognito can also be used to get temporary AWS Credentials in exchange of Authenticated tokens. You can then use this temporary credential to initialise AWS SDK in your browser and access AWS services. We can use this credential to access Api Gateway secured by AWS_IAM. But trick part is, we need to sign our http request with AWS temporary credential which is then verified by AWS for validity before they provide access to Api endpoints.

Following solution uses AWS4 library to sign request, URL library to parse url. But because of issue #46, We tested with forked version of AWS4 . Concept can be simplified as, replace GraphQL links fetch with custom fetch. Inside custom fetch, sign request with AWS4 for needed headers. We referred a small code to bootstrap our custom fetch. Auth.currentCredentials() is a custom function which resolves with temporary AWS credentials. With this setup, you should be able to call graphQL endpoint secured by AWS_IAM.

awsGraphqlFetch(uri, options){
options = options || {};
const signable = {};
const urlObject = url.parse(uri);
signable.host = urlObject.host;
signable.path = urlObject.path;
['method', 'body', 'headers', 'region', 'service'].forEach(key => signable[key] = options[key]);
return Auth.currentCredentials().then((data) => {
const credentials = {
'secretAccessKey': data.secretAccessKey,
'accessKeyId': data.accessKeyId,
'sessionToken': data.sessionToken
};
aws4.sign(signable, credentials);
options.headers = signable.headers;
return fetch(uri, options);
})
}
const client = new ApolloClient({
link: new HttpLink({
uri: "https://XXXXXXX.execute-api.us-east-1.amazonaws.com/production/graphql",
fetch: awsGraphqlFetch
}),
cache: new InMemoryCache()
});

Hopefully, this helps someone else that’s starting up with GraphQL as an AWS lambda using the AWS api gateway and Cognito. I hope it give me some brownie points within my team :)