DEV Community

Hasura for Hasura

Posted on • Originally published at blog.hasura.io on

Add Authentication and Authorization to Vue.js Apps with Auth0 and GraphQL

TL;DR

  • Add authentication to Vue.js app with Auth0
  • Authorization using JWT and Hasura GraphQL permissions
  • A sample Vue app protected by login to fetch articles written by logged in user
  • Source code for sample app

Vue.js + Auth0 + GraphQL + Hasura

Tech Stack

The app uses the following stack that needs to be setup and configured to get it working:

Let's deploy Hasura along with postgres to get our GraphQL APIs ready.

Deploy Hasura

Hasura is an open-source engine that gives you realtime GraphQL APIs on new or existing Postgres databases, with built-in support for stitching custom GraphQL APIs and triggering webhooks on database changes.

Follow the instructions in the docs to deploy Hasura. Note the Heroku URL for GraphQL Endpoint. You will be configuring this in the app later.

Apply the migrations by following the instructions in this section to create the necessary database schema and permissions.

Now the backend is ready! You will be able to instantly query using Hasura GraphQL APIs. The endpoint will look like (https://myapp.herokuapp.com/v1alpha1/graphql). We will come back to this during the integration with the Vue app.

Create an application in Auth0

  1. Head to Auth0 dashboard and create an application in Single Page Web App type.

  1. In the settings of the application, add http://localhost:3000/callback as "Allowed Callback URLs" and http://localhost:3000 as "Allowed Web Origins" to enable local development of the app

Add rules for custom JWT claims

In the Auth0 dashboard, navigate to "Rules". Add the following rules to add our custom JWT claims:

function (user, context, callback) {
  const namespace = "https://hasura.io/jwt/claims";
  context.idToken[namespace] = 
    { 
      'x-hasura-default-role': 'user',
      // do some custom logic to decide allowed roles
      'x-hasura-allowed-roles': user.email === 'admin@foobar.com' ? ['user', 'admin'] : ['user'],
      'x-hasura-user-id': user.user_id
    };
  callback(null, user, context);
}
Enter fullscreen mode Exit fullscreen mode

Get your JWT signing certificate

Head to https://hasura.io/jwt-config and generate the config for your Auth0 domain.

Copy the JWT Config that is generated for the Autho app.

Enable JWT Mode on Hasura

The config generated above needs to be used in the HASURA_GRAPHQL_JWT_SECRET environment variable. We also need to set the HASURA_GRAPHQL_ADMIN_SECRET key for the JWT mode to work.

Once you have added this, the GraphQL endpoints can only be queried using Authorization header or X-Hasura-Admin-Secret header.

Create Auth0 Rule

Everytime user signups on Auth0, we need to sync that user into our postgres database. This is done using Auth0 rules. Create another Rule and insert the following code:

function (user, context, callback) {
  const userId = user.user_id;
  const nickname = user.nickname;

  request.post({
  headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': '<your-admin-secret>'},
  url: 'http://myapp.herokuapp.com/v1alpha1/graphql',
  body: `{\"query\":\"mutation($userId: String!, $nickname: String) {\\n insert_users(\\n objects: [{ auth0_id: $userId, name: $nickname }]\\n on_conflict: {\\n constraint: users_pkey\\n update_columns: [last_seen, name]\\n }\\n ) {\\n affected_rows\\n }\\n }\",\"variables\":{\"userId\":\"${userId}\",\"nickname\":\"${nickname}\"}}`
}, function(error, response, body){
    console.log(body);
    callback(null, user, context);
});
}
Enter fullscreen mode Exit fullscreen mode

Replace the admin secret and url appropriately.

Finally we have the full backend and auth setup ready. Let's configure the Vue.js frontend to make the GraphQL query with the right headers.

Configure Vue-CLI-Apollo-Plugin

We will be using the Auth0's sample app to get started with boilerplate code.

The following command generates apollo client setup for a Vue app.

vue add apollo
Enter fullscreen mode Exit fullscreen mode

This will generate a file called vue-apollo.js in src. In this file, we will be configuring the options object, getAuth by defining the following:

getAuth: tokenName => {
    // get the authentication token from local storage if it exists
    // return the headers to the context so httpLink can read them
    const token = localStorage.getItem('apollo-token')
    if (token) {
      return 'Bearer ' + token
    } else {
      return ''
    }
},
Enter fullscreen mode Exit fullscreen mode

This configuration ensures that ApolloClient makes use of the token returned by Auth0 for Authorization header when making its query or subscription.

Authenticated Query

Apollo Client has been configured with the right headers in the above setup. So let's add a simple query to fetch list of articles written by the user who is logged in.

export default {
  apollo: {
    // Simple query that will update the 'article' vue property
    article: gql`query {
      article {
        id
        title
      }
    }`,
  },
}
Enter fullscreen mode Exit fullscreen mode

Now we would like to show this only if the user is logged in to the app.

So in our <template> tag of Home.vue, we will be using the following code snippet to list the articles

<template>
    ...
    ...
    <div v-if="isAuthenticated">
      <h1 class="mb-4">
        Articles written by me
      </h1>
      <div v-for="a in article" :key="a.id">
        {{a.id}}. {{ a.title }}
      </div> 
    </div>
    ...
    ...
</template>
Enter fullscreen mode Exit fullscreen mode

Note that we are ensuring that this markup has to be rendered only if isAuthenticated returns true. To implement this, we emit an event after each successful login.

Head to src/auth/authService.js to see the implementation details of Auth0 login and event emitting.

In this file, an event is emitted once the login is done successfully.

this.emit(loginEvent, {
  loggedIn: true,
  profile: authResult.idTokenPayload,
  state: authResult.appState || {}
});
Enter fullscreen mode Exit fullscreen mode

A plugin has been registered to handle this event in src/plugins

import authService from "../auth/authService";

export default {
  install(Vue) {
    Vue.prototype.$auth = authService;

    Vue.mixin({
      created() {
        if (this.handleLoginEvent) {
          authService.addListener("loginEvent", this.handleLoginEvent);
        }
      },

      destroyed() {
        if (this.handleLoginEvent) {
          authService.removeListener("loginEvent", this.handleLoginEvent);
        }
      }
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

So once a loginEvent occurs, handleLoginEvent method is called.

And in our Home.vue component, we handle that method to update isAuthenticated value. It is false by default and once login is successful, gets updated to true.

methods: {
    handleLoginEvent(data) {
      this.isAuthenticated = data.loggedIn;
      this.isLoading = false;
    }
},
Enter fullscreen mode Exit fullscreen mode

The GraphQL query above is sent using the token header returned by Auth0 and this takes care of the Authentication.

Authorization using JWT

Though the user is logged in, we want to show only the articles written by the same user. The permissions have been configured in such a way that only the user who wrote the article will be able to fetch the data.

Head to the Heroku app URL to open Hasura console and navigate to Data->article->Permissions to see the permissions defined for the user role.

The permission check looks like:

{ "user_id": {"_eq": "X-Hasura-User-Id"}}
Enter fullscreen mode Exit fullscreen mode

This means that when a request is being sent with Authorization: Bearer <token> from the client, it will look for the X-Hasura-User-Id value from the token payload and filter it for the user_id column, ensuring that only logged in users get the data and also get only their data. The user has permissions to access all columns.

Protected Routes using Vue Router

Since we are using Vue Router, we can add Navigation Guards using a Global Before Guard. This is called whenever a navigation is triggered and the navigation is considered pending until resolved.

In src/router.js, we define the beforeEach guard which checks for the boolean auth.isAuthenticated() before resolving.

router.beforeEach((to, from, next) => {
  if (to.path === "/" || to.path === "/callback" || auth.isAuthenticated()){
    return next();
  }

  auth.login({ target: to.path });
});
Enter fullscreen mode Exit fullscreen mode

In case, the page is not /, /callback or the user is not authenticated, then the user is redirected to Login page using auth.login method.

Running the App

We need to configure the Hasura GraphQL Endpoint in the Vue.js app. Go to src/vue-apollo.js and modify the httpEndpoint and wsEndpoint values appropriately.

Run the sample app by running the following commands:

npm install
npm run serve
Enter fullscreen mode Exit fullscreen mode

You should be seeing a screen like this:

I have put together a boilerplate so that you can get started quickly!

Check it out on github.

Take it for a spin and let us know what you think. If you have any questions or run into any trouble, feel free to reach out to us on twitter, github or on our discord server.


This article was originally published on the Hasura blog and was authored by Praveen Durairaj.

Top comments (0)