Codementor Events

Handling authentication in your GraphQL-powered Vue app

Published Jun 24, 2020Last updated Dec 20, 2020
Handling authentication in your GraphQL-powered Vue app

The primary goal of authentication is identifying the person requesting a resource. It is a very tricky subject when developing apps as there is no “one size fits all” rule for handling authentication in our apps.

Handling authentication on the client in a web-based application is even more tricky as there are security concerns such as cross-site scripting (XSS) where an attacker accesses information stored in the browser and uses it to masquerade as the user. Most SPAs implement a token-based authentication because tokens are stateless and they scale easily as it takes away the stress of your server keeping track of session state.

The flow for authenticating users in modern apps is usually in this format:

  • The client sends a request to the authentication route with user information like email address and password
  • The server checks the identity of the user, creates a JSON web token (JWT), and sends it back to the browser
  • The client stores the token into one of the browser storage mediums(APIs)
  • The client appends the token to the authorization header to make subsequent requests to the server

Modern apps authentication flow

There are three storage options available for saving a token on the client, they include:

  • Local storage
  • Session storage
  • Cookies

In this tutorial we’re going to take a look at how to handle authentication in a Vue app connected to a GraphQL API, we will do so by building a mini app. We will be making use of localStorage to store our token.

The API we will be connecting to can be found here.

Prerequisites

This tutorial assumes the reader has the following:

You can install Vue CLI with the following command using Yarn:

yarn global add @vue/cli

Tools we will be using to build our app include:

Click here to see the full demo with network requests

Vue-Apollo — This is an Apollo Client integration for Vue.js, it helps integrate GraphQL in our Vue.js apps!

Vuex — Vuex is a state management pattern library for Vue.js applications, it serves as a centralized store for all the components in an application. It is heavily influenced by the Flux architectural pattern created by Facebook.

Vue Router — This is the official routing library for Vue.js, it makes routing in our Vue.js applications easier.

Getting started

We will be using the Vue CLI tool to bootstrap a new Vue project, this tool helps us with not having to worry about configurations to get started with using our app as we can manually select the needed packages for our app.

First, we create a new project using the create command:

vue create blogr

Move your down arrow key to “manually select features”, press enter and choose the following features:

cli options

Next, change directory into the project folder with this command:

cd blogr

Start your project with the command:

yarn serve

You should see your app running on http://localhost:8080 after running the yarn serve command.

vue running app

Creating the user interface

Open your App.vue file located in your src folder and remove the following lines of code:

<div id="nav">
  <router-link to="/">Home</router-link> |
  <router-link to="/about">About</router-link>
</div>

Replace the content removed with the following:

<header class="header">
  <div class="app-name">Blogr</div>
    <div v-if="authStatus" id="nav">
      <div>{{user.name}}</div>
      <button class="auth-button" @click="logOut" > Log Out</button>
    </div>
</header>

We’re getting the name of the authenticated user and we’ve created a logout button that triggers a logOut method.

Next, navigate to src/views and create a Register.vue file and include the following lines of code in the file:

<template>
  <div class="auth">
    <h3>Sign Up</h3>
    <form action="POST" @submit.prevent="registerUser">
      <label for="name"> Name</label>
      <input type="text" name="name" placeholder="John Doe" v-model="authDetails.name" />
      <label for="email">Email Address</label>
      <input type="email" name="email" placeholder="yourdopeemail@something.com" v-model="authDetails.email" />
      <label for="password">Password</label>
      <input type="password" name="password" placeholder="password" v-model="authDetails.password" />
      <button class="auth-submit">submit</button>
     <p class="auth-text"> Already have an account? <router-link to="/login"> Login </router-link> </p>
    </form>
  </div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
  name: 'Register',
  data () {
    return {
      authDetails: {
        name: '',
        email: '',
        password: ''
      }
    }
  },
  methods: {
    registerUser: function () {

    }
  }
}
</script>

In this code block, we’ve created the signup page without any functionality, clicking the submit button triggers the registerUser method which does nothing for now.

We’re using the v-model to create a two-way data binding on our input boxes to authDetails, if the value of our form changes, the value in authDetails changes alongside it.

Let’s add some style to our app, create a styles folder in /src/assets. Inside the src folder create an index.css file and include the following:

.header {
    display: flex;
    justify-content: space-between;
    background-color: fuchsia;
    height: 25%;
    padding: 1rem;
}
.app-name {
    font-weight: 900;
    font-size: 3rem;
}
.auth {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.auth h3 {
    margin-top: 2rem;
}
form {
    max-width: 50%;
    margin-top: 1rem;
    padding: 4rem;
    border: 1px solid #c4c4ce;
}
form input {
    display: block;
    margin-bottom: 1.2rem;
    padding: 0.4rem 1.2rem;
    background-color: white;
}
.auth-submit {
    margin-top: .5rem;
    padding: .5rem 1rem;
    border: none;
    background-color: fuchsia;
    color: white;
    font-weight: bold;
    text-transform: capitalize;
    border-radius: 0.3rem;
}
.auth-text a {
    color: black;
    text-decoration: none;
}
.auth-text a:visited {
    color: inherit;
}
.auth-text a:hover {
    text-decoration: underline;
}
.auth-text {
    margin-top: .5rem;
}
.auth-button{
    margin: .7rem 2rem 0 0;
    padding: .5rem 2rem;
    background-color: white;
    border: none;
    border-radius: .3rem;
}
main{
    margin-top: 5rem;
    display: flex;
    justify-content: center;
}

Next, let’s build the login page, create a Login.vue file in src/views and include the following in it:

<template>
  <div class="auth">
    <h3>Log In</h3>
    <form action="POST" @submit.prevent="loginUser">
      <label for="email">Email Address</label>
      <input type="email" name="email" placeholder="yourdopeemail@something.com" v-model="authDetails.email" />
      <label for="password">Password</label>
      <input type="password" name="password" placeholder="password" v-model="authDetails.password" />
      <button class="auth-submit">submit</button>
     <p class="auth-text"> Don't have an account? <router-link to="/"> Register </router-link> </p>
    </form>
  </div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
  name: 'Login',
  data () {
    return {
      authDetails: {
        email: '',
        password: ''
      }
    }
  },
  methods: {
    loginUser: function () {

    }
  }
}
</script>

This page is similar to our Register.vue page, clicking on the submit button triggers the loginUser method, which does nothing for now.

Next, replace the contents of Home.vue with the following:

<template>
  <div class="home">
    <main>
     Yaay! User authenticated!
    </main>
  </div>
</template>
<script>
// @ is an alias to /src

export default {
  name: 'Home',
  components: {
  },
  computed: {

  }
}
</script>

This page will serve as our dashboard page that will be displayed to our user when they are authenticated:

home vue

Configuring the routes

Next let’s include the routes for the login, register, and dashboard page in our router file located in src/router/.

Remove the contents in the routes array and add the following to the index.js file:

{
   path: '/dashboard',
   name: 'Home',
   component: () => import('@/views/Home.vue'),
 },
 {
   path: '/login',
   name: 'Login',
   // route level code-splitting
   // this generates a separate chunk (login.[hash].js) for this route
   // which is lazy-loaded when the route is visited.
   component: () => import(/* webpackChunkName: "login" */ '@/views/Login.vue')
 },
 {
   path: '/',
   name: 'Register',
   // route level code-splitting
   // this generates a separate chunk (register.[hash].js) for this route
   // which is lazy-loaded when the route is visited.
   component: () => import(/* webpackChunkName: "register" */ '@/views/Register.vue')
 },
 {
   path: '*',
   redirect: 'login'
 }

These routes take advantage of Webpack’s code-splitting and are lazy-loaded, this inherently improves our app performance.

We also added a * – this is known as a wildcard router. The router will select this route if the requested URL doesn’t match any of the defined routes and the user will be redirected to the login page.

Our App should now look similar to this when you visit localhost:8080:

registervue

Installing Apollo Client with Vue-Apollo

Apollo Client is a complete GraphQL client for your UI framework, it helps you connect to, retrieve data, and modify data in a GraphQL server.

To integrate Apollo in our Vue app we will have to install the vue-apollo plugin for vue-cli:

vue add apollo

vue install plugin

This plugin creates two files, apollo.config.js in the root directory of the project and vue-apollo.js in the src folder, it also injects Apollo provider in the Vue instance in main.js.

This provider makes it possible to use Apollo client instances in our Vue components. Next, let’s make some configurations to our vue-apollo.js file located in our /src folder.

Include the following at the top of the file contents:

import { setContext } from 'apollo-link-context'

This helps us make use of setContext method when adding an authorization header to our HTTP requests.

Next, we change the httpEndpoint we would be connecting to. Replace the value of your httpEndpoint variable with this:

const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'https://bloggr-api.herokuapp.com/'

Add the following immediately after the httpEndpoint is defined:

const authLink = setContext(async (_, { headers }) => {
  // get the authentication token from local storage if it exists
  const token = JSON.parse(localStorage.getItem('apollo-token'))
  // Return the headers to the context so httpLink can read them
  return {
    headers: {
      ...headers,
      authorization: token || ''
    }
  }
})

Next, we override the default Apollo link with our authLink, place the following in the defaultOptions object:

link: authLink

The defaultOptions object sets application-wide default values for apolloClient.

Let’s proceed to create our apolloClient instance with our defaultOptions object as a value, we are exporting it with the export so we can access apolloClient in our vuex store:

export const { apolloClient, wsClient } = createApolloClient({
  ...defaultOptions
  // ...options
})

Next, replace the createProvider function with the following:

export function createProvider () {
  // Create vue apollo provider
  const apolloProvider = new VueApollo({
    defaultClient: apolloClient,
    defaultOptions: {
      $query: {
        fetchPolicy: 'cache-and-network'
      }
    },
    errorHandler (error) {
      // eslint-disable-next-line no-console
      console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
    }
  })
  return apolloProvider
}

The createProvider function gets called in the main.js file as soon as our app is initialized, it injects Apollo client instances into our Vue app and makes it possible to use Apollo in our components.

Queries and mutations

Create a folder named graphql in your /src folder, inside it create two files with the following command:

touch queries.js mutations.js

The queries.js file will contain queries to be made to our GraphQL server, a Query is a request made to the API to retrieve data. Queries are similar to HTTP GET requests in REST APIs.

The mutations.js file would contain mutations made to the GraphQL server, Mutations are queries that change the data state in your Apollo server. Mutations are similar to HTTP PUT, POST, or DELETE request in REST APIs.

Next, add the following lines of code in our mutations.js file:

import gql from 'graphql-tag'
export const LOGIN_USER = gql`
mutation login ($email: String! $password: String! ){
  login(email: $email password: $password ){
    token
  }
}
`
export const REGISTER_USER = gql`
mutation createUser($name: String! $email: String! $password: String! ) {
    createUser( name: $name, email: $email, password: $password) {
      token
    }
}
`

gql helps us write our GraphQL queries, we’ve created the mutations for logging in and creating a new user, the contents of our form serves as the variables for our mutations.

In our queries.js file, Include the following query, the query gets the current authenticated user:

import gql from 'graphql-tag'

export const LOGGED_IN_USER = gql`
  query {
    me {
      id
      name
      email
    }
  }
`

Configuring Vuex

First, let’s import our Mutations, Queries , and the apolloClient instance:

import { apolloClient } from '@/vue-apollo'
import { LOGGED_IN_USER } from '@/graphql/queries'
import { LOGIN_USER, REGISTER_USER } from '@/graphql/mutations'

Importing the apolloClient instance makes us able to perform Apollo operations in our store.

Next, set the data we will be needing in our state, put the following in the state object:

token: null,
user: {},
authStatus: false

The state object is the central store for data that will be used application-wide. It represents a “single source of truth”.

The authStatus is a boolean that tells if a user is authenticated or not, the user object would contain the details of an authenticated user.

Next, we configure our getters, include the following in the getters object:

isAuthenticated: state => !!state.token,
authStatus: state => state.authStatus,
user: state => state.user

Getters help with retrieving items in our state object, a getter’s result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.

Proceed to create new mutations, in the mutations object:

SET_TOKEN (state, token) {
  state.token = token
},
LOGIN_USER (state, user) {
  state.authStatus = true
  state.user = { ...user }
},
LOGOUT_USER (state) {
  state.authStatus = ''
  state.token = '' && localStorage.removeItem('apollo-token')
}

We’ve created mutations to change state in a Vuex store, mutation functions are synchronous and they typically take two parameters — the state object and a payload which can be a variable or an object.

Finally, let’s configure our actions, actions are asynchronous functions used to commit mutations. Actions are triggered with the store.dispatch method:

async register ({ commit, dispatch }, authDetails) {
     try {
       const { data } = await apolloClient.mutate({ mutation: REGISTER_USER, variables: { ...authDetails } })
       const token = JSON.stringify(data.createUser.token)
       commit('SET_TOKEN', token)
       localStorage.setItem('apollo-token', token)
       dispatch('setUser')
     } catch (e) {
       console.log(e)
     }
   },
   async login ({ commit, dispatch }, authDetails) {
     try {
       const { data } = await apolloClient.mutate({ mutation: LOGIN_USER, variables: { ...authDetails } })
       const token = JSON.stringify(data.login.token)
       commit('SET_TOKEN', token)
       localStorage.setItem('apollo-token', token)
       dispatch('setUser')
     } catch (e) {
       console.log(e)
     }
   },
   async setUser ({ commit }) {
     const { data } = await apolloClient.query({ query: LOGGED_IN_USER })
     commit('LOGIN_USER', data.me)
   },
   async logOut ({ commit, dispatch }) {
     commit('LOGOUT_USER')
   }

Now that our store is configured, let’s add functionality to our login and register forms, include the following in the script section of your Register.vue file:

<script>
import { mapActions } from 'vuex'
  ....
  methods: {
    ...mapActions(['register']),
    registerUser: function () {
      this.register(this.authDetails)
        .then(() => this.$router.push('/dashboard'))
    }
  }
...

To dispatch actions in our component, we’re using the mapActions helper which maps component methods to this.$store.dispatch.

The code above sends the form details as a payload to the register action in our Vuex store and then changes the route to /dashboard.

Include the following in your Login.vue file:

<script>
import { mapActions } from 'vuex'
....
  methods: {
    ...mapActions(['login']),
    loginUser: function () {
      this.login(this.authDetails)
        .then(() => this.$router.push('/dashboard'))
    }
  }
...

Include the following in the script section of your Home.vue file to get user details:

<script>

import { mapGetters } from 'vuex'
....
  computed: {
    ...mapGetters(['user'])
  }
....
</script>

The mapGetters helper simply maps store getters to local computed properties.

Guarding routes

Import the vuex store at the top of your router file:

import store from '../store'

Add a meta field to our /dashboard route, this meta helps us when defining our routes navigation guard middleware. Our dashboard route record will look similar to this:

{
  path: '/dashboard',
  name: 'Home',
  component: () => import('@/views/Home.vue'),
  meta: { requiresAuth: true }
},

Include the following just before export default router:

router.beforeEach((to, from, next) => {
    // Check if the user is logged i
  const isUserLoggedIn = store.getters.isAuthenticated
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!isUserLoggedIn) {
      store.dispatch('logOut')
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next()
  }
})

This defines our navigation guard for our route records. When we navigate to any route with the requiresAuth meta field it checks if the user is authenticated and authorized to access that route and redirects the user to the login page if the user is not authorized.

Our finished application should look similar to this:

vue app finished

Conclusion

In this post, we’ve seen how to handle authentication of our GraphQL APIs with vue-router, vue-apollo, and Vuex. You can learn more about Apollo GraphQL here, you can also learn more about GraphQL on the LogRocket blog. Check out the repository for this tutorial on GitHub, it can be used as a boilerplate to scaffold your app. You can also check out the GraphQL API repository and the deployed version of our app.

First published on Logrocket Blog: https://blog.logrocket.com/handling-authentication-in-your-graphql-powered-vue-app/

Discover and read more posts from Anjolaoluwa Adebayo-Oyetoro
get started