loading...
Hasura

Using Amplify CLI for adding Auth to your Hasura GraphQL engine

vladimirnovick profile image Vladimir Novick ・8 min read

Let's assume you already played around with Hasura open source GraphQL engine and you were wondering how to add an authentication layer with AWS Cognito. Couple of words about Hasura if you are not familiar. Hasura is free and open source engine that auto generates GraphQL API on top of new or existing Postgres database. If this is the first time you hear about it, I suggest to check my introductory article here:

and if you were wondering about how to use Authentication with Hasura you can read about it here:

In last article I showed AWS Cognito as one of solutions how to create your authentication for your app and pass Auth token to Hasura. In this article I want to explain how you do that with aws-amplify CLI as well as how you connect your users to actual cognito user pool users.

In a nutshell we will cover

  1. How to deploy Hasura
  2. How to setup aws amplify with aws cognito
  3. How to setup a AWS lambda to update our database after user is created in cognito
  4. Customise cognito JWT creation to enable hasura permission rules

Table of Contents

Creating and deploying our engine

Let's start by regular step of Deploying to Heroku

Screenshot Image

We also can use AWS RDS to deploy our engine which is described in detail here

Now before we setting any auth or continue in general we will secure our endpoint by setting HASURA_GRAPHQL_ADMIN_SECRET

Screenshot Image

Setting up Hasura CLI

We want changes done in our console to be reflected as migrations files and stored in source control as explained in detail here

What we will do is set HASURA_GRAPHQL_ENABLE_CONSOLE to false in our heroku config vars

Screenshot Image

Now it's time to init our hasura project with through Hasura CLI

hasura init --endpoint https://aws-sample.herokuapp.com

And then

cd hasura
hasura console --admin-secret=<your admin secret>

Console will open up on localhost and whatever you do in the console will be saved in local migration files that you can store in source control.

Setting our database structure

Now when we have our engine set up, we need to create database structure. We will create simple blog cms so we will have only posts and users table. Important part is that users table id have to be of a text format so we can wire it up with AWS user pool properly.

Posts table will look like this:
Screenshot

User table will look like this:
Screenshot

I also added object relationship from posts userId to user object

Screenshot

so we can query our users from posts query like so

query {
  posts {
    title
    content
    user {
      name
    }
  }
}

Now when our backend ready it's time to set up our frontend and authentication.
Now I don't know what frontend framework you use, so I will use React for this example, but the process is similar in Vue and Angular. I hardly will touch Frontend here. My main goal is so you will understand the flow.

Setting our Auth with Amplify CLI

npm install -g @aws-amplify/cli
amplify configure

This will open up the AWS console. Once you are logged in to the console you can jump back to the command line. Then we need to specify AWS region.

In the next step we will need to specify a new AWS IAM user we about to create.
It will open up IAM dashboard with the user we already set up with the one we specified through CLI.


Next we will need to set up AWS permissions. I will just click Next, Review and will create a user. You will be presented with a screen with Access key and secret which you should type in amplify cli

Creating your Frontend

We will use Amplify auth module with React, but you can choose your own favorite framework. Check Amplify Vue docs and Angular docs for more detail.

we will start by creating our frontend with

npx create-react-app react-frontend

and then run

amplify init

To add basic authentication with User pools run amplify add auth with default configuration. After adding auth we need to deploy it to AWS. This can be done by running amplify push which will update our resources in the cloud and will add Cognito User Pool

It's time to add auth to our React app:

yarn add aws-amplify aws-amplify-react

In our App.js file let's wrap our app with Amplify withAuthenticator HOC

import Amplify, { Auth } from 'aws-amplify';
import awsmobile from './aws-exports';
import { withAuthenticator } from 'aws-amplify-react'; // or 'aws-amplify-react-native';

Amplify.configure(awsmobile);


export default withAuthenticator(App, true);

What we will be presented is AWS auth screen where we will have sign up, sign in etc. We won't cover how to customize everything for your app, so let's just use the default one

Now let's add some behavior on sign up/sign in.
Whenever our App is authenticated, basically our App component will be rendered.
So if I will add this code to our App.js:

import { Auth } from 'aws-amplify'

//....rest of code

componentDidMount(){
    Auth.currentAuthenticatedUser().then(user => {
      console.log(user)
    })
}

//rest of code

you will be able to get user data.

So far we have authentication, but there is a problem. We need to add this auth data to our Hasura.

Adding a new User pool user into Hasura

If we open our AWS console on User pool tab we will be able to see that we can create custom Triggers for different stages in authentication process. We will need to create at least two triggers. Pre sign-up and pre token generation.

Let's start with sign up trigger

Pre Sign Up Lambda

Let's go to AWS Lambda console and create a new lambda. You will have to create it locally first because we will have node-fetch dependency, so we will need to upload a zip to AWS console. Our lambda index.js file will look like this:

const fetch = require('node-fetch');

const adminSecret = process.env.ADMIN_SECRET;
const hgeEndpoint = process.env.HGE_ENDPOINT;

const query = `
mutation addUser($id: String!, $phone:String!, $email: String!){
  insert_users(objects:[{
    email: $email
    id: $id
    phone: $phone
  }]) {
    returning {
      email
    }
  }
}
`

exports.handler = (event, context, callback) => {

try {
  const qv = {
    email: event.request.userAttributes.email, 
    phone: event.request.userAttributes.phone_number,
    id: event.userName
  };

  fetch(hgeEndpoint + '/v1alpha1/graphql', {
      method: 'POST',
      body: JSON.stringify({query: query, variables: qv}),
      headers: {'Content-Type': 'application/json', 'x-hasura-admin-secret': adminSecret},
  })
  .then(res => res.json())
  .then(json => {
      callback(null, event);
  });


} catch (e) {
  callback(null, false)
}


};

What we are trying to do here is to insert a new user to database by executing mutation. You can notice that we pass our Admin Secret at headers, but this is fine for server to server communication. What is happening here is that our id will be cognito username

Let's try to Sign Up now.

We will get notification mail with our verification code

Now we can sign in and see our Auth token in the console.

The cool part is also that we will have our user created in Postgres. Let's check our console:

Pre Token Generation lambda

We are getting our token, but it's not enough. If we want to use JWT_TOKEN with Hasura we need to do several things. (I explained JWT auth in details here):

Set HASURA_GRAPHQL_JWT_SECRET

Set HASURA_GRAPHQL_JWT_SECRET as env variable in our Heroku. Our config will be:

{
"type":"RS256",
    "jwk_url": "https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-    known/jwks.json",
"claims_format": "stringified_json"
}

Note that {userPoolId} should be substituted to your userPoolId taken from AWS console as well as {region} to correct region you are using

Create our Pre Token generation lambda.

For our pre token generation lambda we need to add custom claims. We will pass username as user id so we will be able to reference it in Hasura permission system

exports.handler = (event, context, callback) => {
    event.response = {
        "claimsOverrideDetails": {
            "claimsToAddOrOverride": {
                "event": JSON.stringify(event.userName),
                "https://hasura.io/jwt/claims": JSON.stringify({
                    "x-hasura-allowed-roles": ["anonymous","user", "admin"],
                    "x-hasura-default-role": event.userName === "vnovick" ? "admin": "user",
                    "x-hasura-user-id": event.userName
                })
            }
        }
    }
    callback(null, event)
}

Let's copy signInUserSession.idToken.jwtToken from Auth.currentAuthenticatedUser() response and paste it in Hasura console as Authorization Bearer token

Now all custom claims will be passed to Hasura permissions system and we will be able to reference them there.

Create Roles in Hasura Permission system

Let's see how it's done. We will create two additional roles - anonymous without any permissions and a user role.

I want user to be able to query only their own user data as well as see only their posts. We can set up permissions in the console accordingly:

user role will be able to see only their own data.

Let's double check that by querying all users:

Summary

This blog post is only a brief overview of what you can do with Hasura and AWS Cognito, but I hope you get the idea of main auth workflow you should stick to. As you can see, because we use Lambda triggers and plug them into Cognito auth process we can do any level of complexity. We can validate if our user exists, add custom variables to hasura custom claims and so on.

After reading this blog post I will appreciate if you can give me feedback what parts you want me to cover more in depth so I can either write another more advanced blog post or lead you to resources explaining these parts.

Posted on by:

vladimirnovick profile

Vladimir Novick

@vladimirnovick

CTO and Co-Founder at Event Loop, Google Developer Expert, consultant, worldwide speaker, published author, workshops teacher, software architect and developer in Web/Mobile/AR/VR/IoT/AI fields

Hasura

Hasura GraphQL Engine is an opensource service that gives you instant realtime GraphQL APIs on Postgres with fine grained access control.

Discussion

pic
Editor guide
 

A better trigger would be the Post Confirmation trigger instead of the Pre sign-up one. The latter wouldn't be useful if the user creation fails in Cognito but the former one guarantees that the user exists in Cognito BEFORE they can be created in the database.

 

That's a good suggestion. I will make sure to try it out. Thanks!

 

Thanks for this, Vladimir. A few questions. How did you get your env variables into the lambda?

Also, is this the dir structure you are working with?

.
├── hasura
│   ├── config.yaml
│   └── migrations
│       ├── 1556043822200_create_table_public_posts.down.yaml
│       ├── 1556043822200_create_table_public_posts.up.yaml
│       ├── 1556043910299_create_table_public_users.down.yaml
│       ├── 1556043910299_create_table_public_users.up.yaml
│       ├── 1556043996415_set_fk_public_posts_"userId".down.yaml
│       └── 1556043996415_set_fk_public_posts_"userId".up.yaml
└── react-frontend
    ├── README.md
    ├── amplify
    │   ├── #current-cloud-backend
    │   ├── backend
    │   └── team-provider-info.json
    ├── package.json
    ├── public
    │   ├── favicon.ico
    │   ├── index.html
    │   └── manifest.json
    ├── src
    │   ├── App.css
    │   ├── App.js
    │   ├── App.test.js
    │   ├── aws-exports.js
    │   ├── index.css
    │   ├── index.js
    │   ├── logo.svg
    │   └── serviceWorker.js
    └── yarn.lock

Or did you set the env vars in your cloudformation manually?

Thank you, sir.

 

Thanks for the great tutorial. I managed to get this working easily, but I'm having a lot of trouble with the next step, which is connecting my newly authenticated app to Hasura via Apollo. The token I get works fine if I paste it into Hasura, but when I set up Apollo to send requests to Hasura I get the following error:

Error: GraphQL error: Malformed Authorization header

I don't know what the problem is or whether it's Hasura or Apollo or Cognito, but if I console log the jwt before passing it to Apollo I can paste it into Hasura and it works fine. Is there a step I'm missing? Here's my code, pretty much right out of the Apollo docs example:

async function getSession() {
Auth.currentSession().then(res=>{
let idToken = res.getIdToken()
let jwt = idToken.getJwtToken()
//You can print them to see the full objects
console.log(JSON.stringify(idToken))
console.log(jwt)
return jwt
})
}

const token = getSession()

const client = new ApolloClient({
uri: 'api.example.com/v1/graphql',
headers: {
authorization: token ? Bearer ${token} : "",

}});

//backtics don't show in headers section

 

getSession is async function and you missed await keyword calling getSession. Your token is a promise.

 

Hi, can you elaborate on when you say this:

"Let's copy signInUserSession.idToken.jwtToken from Auth.currentAuthenticatedUser() response and paste it in Hasura console as Authorization Bearer token"

 

If you are caught up on setting up the lamda, here is an example using hasura. github.com/vnovick/bad-words-lambd.... Vnovick comes thru again!