Hasura + GoTrue = ❤️
This is a slightly extended version of Netlify's GoTrue. It includes a docker-compose.yaml
file to deploy it together with Hasura. This set up uses two databases, PostgreSQL for Hasura and MariaDB for GoTrue. Have fun!
Background
Setting up Hasura is very easy. However, you immediately get disappointed that you don't get authentication out of the box. You have to read through a lot of tutorial blogs just to end up using paid options or Firebase.
GoTrue is a simple yet solid authentication and user management tool. It is very light and straightforward. You can sign up users, verify them and also help them reset their passwords. The means of authentication is JWT, meaning that after signing up, a client sends a username and password to the system and they get an access_token
and a refresh_token
. This way, the client can be able to attach the token each time it makes a request.
Hasura is a very neat GraphQL engine built on top of PostgreSQL using Elixir. This gives you highly scalable GraphQL APIs that you can consume using web, mobile and desktop applications written in various programming languages. Hasura is nice in that it gives you room for multiple authentication and authorization options including JWT and WebHooks.
When you send a JWT to hasura in form of Authorization: Bearer token
, Hasura engine checks the signature of the token using a preconfigured secret. Therefore, it doesn't have to send the token to the issuer from the server side to confirm its authenticity, Hasura simply checks the signature and, if it checks out, proceeds to authorize the request according to the claims available inside the token.
Which brings us to the claims and GoTrue. Hasura requires some custom claims which are not available by default inside the tokens generated by GoTrue. So, I simply took GoTrue source code and extended the token.go
file to include Hasura claims inside the generated token.
This means the rest of GoTrue code works as expected and will be easy to update in future. The added code is less than 10 lines, and I am new to Go, so perhaps there is an even shorter way of adding the feature.
I also included the deployment file I use, this should work out of the box, but there is a catch.
Deployment
Clone this repositoy, modify the .env
file to include your preferred details for passwords, domain and smtp settings for emails then run docker-compose up -d
.
The docker-compose.yaml
file looks like this:
version: "3"
services:
postgres:
image: postgres:12
restart: always
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: pgpassword
hasura:
image: hasura/graphql-engine:v1.3.3
ports:
- "5000:5000"
depends_on:
- "postgres"
restart: always
environment:
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:pgpassword@postgres:5432/postgres
## enable the console served by server
HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE: "true"
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: public
HASURA_GRAPHQL_SERVER_PORT: 5000
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to set an admin secret
HASURA_GRAPHQL_ADMIN_SECRET: AdminSecretHere
HASURA_GRAPHQL_JWT_SECRET: '{"type": "HS256", "key": "changethismorethan32characterstring"}'
caddy:
image: abiosoft/caddy
depends_on:
- "web"
restart: always
environment:
ACME_AGREE: "true"
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/Caddyfile
- caddy_certs:/root/.caddy
db:
image: mariadb:10
restart: unless-stopped
env_file: .env
environment:
MYSQL_ROOT_PASSWORD: dbpassword
volumes:
- mariadb:/var/lib/mysql
ports:
- 3306
gotrue:
build: gotrue
restart: unless-stopped
env_file: .env
environment:
- PORT=${GOTRUE_PORT}
- "DATABASE_URL=${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(db:3306)/${MYSQL_DATABASE}?parseTime=true&multiStatements=true"
ports:
- ${GOTRUE_PORT}:${GOTRUE_PORT}
depends_on:
- db
volumes:
caddy_certs:
db_data:
mariadb:
The Caddy server Caddyfile
looks like following:
hasura.example.com {
bind {$ADDRESS}
proxy / hasura:5000 {
transparent
}
tls email@example.com
}
gotrue.example.com {
bind {$ADDRESS}
proxy / gotrue:9999 {
transparent
}
tls email@example.com
}
_Modify the docker-compose.yaml
file and the Caddyfile
to suite your set up.
You will then need to manually run migrations inside the running gotrue
container by doing:
docker-compose exec gotrue /bin/ash
gotrue migrate
The first command brings up the shell of the gotrue
container that is running. The gotrue
image is built from alpine, therefore the default shell is ash
, don't bother looking for zsh
or bash
.
The second command is what you will run inside the container, this will run the migrations and you are now ready to test.
Testing
Sign Up a User
Curl:
curl --location --request POST 'https://gotrue.example.com/signup' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "user@email.com",
"password": "password"
}'
JavaScript:
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({"email":"email@example.com","password":"password"});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch("https://gotrue.example.com/signup", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
Get a Token for This User
curl --location --request POST 'https://gotrue.example.com/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=email@example.com' \
--data-urlencode 'password=password'
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("grant_type", "password");
urlencoded.append("username", "email@example.com");
urlencoded.append("password", "password");
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: urlencoded,
redirect: 'follow'
};
fetch("https://gotrue.example.com/token", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"token_type": "bearer",
"expires_in": 864000,
"refresh_token": "aWF0IjoxNTE2MjM5-2kNw"
}
Using the Token in graphQL requests to Hasura
Given the token above, when making a request to Hasura, add the access token provided for example:
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
Top comments (2)
Starting with Hasura v1.3.3 you can actually specify a claims-map config for this use-case.
Would using this config without modifying GoTrue have the same effect?
Aha... This is very neat. I had not looked at this part of the documentation. Thanks a lot!