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
Tech Stack
The app uses the following stack that needs to be setup and configured to get it working:
- Vue.js with vue-cli-plugin-apollo and vue-router
- Auth0 for authentication
- Hasura GraphQL Engine for instant GraphQL APIs
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.
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.hasura.app/v1/graphql). We will come back to this during the integration with the Vue app.
Create an application in Auth0
- Head to Auth0 dashboard and create an application in Single Page Web App type.
2. 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 === '[email protected]' ? ['user', 'admin'] : ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}
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 Auth0 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);
});
}
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
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 ''
}
},
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
}
}`,
},
}
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>
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 || {}
});
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);
}
}
});
}
};
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;
}
},
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"}}
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 });
});
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
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.