DEV Community

Cover image for Handling authentication in your GraphQL powered Vue app
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Handling authentication in your GraphQL powered Vue app

Written by Anjolaoluwa Adebayo-Oyetoro✏️

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.

LogRocket Free Trial Banner

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
Enter fullscreen mode Exit fullscreen mode

Tools we will be using to build our app include:

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Start your project with the command:

yarn serve
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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/'
Enter fullscreen mode Exit fullscreen mode

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 || ''
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

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

link: authLink
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
`
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

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')
   }
Enter fullscreen mode Exit fullscreen mode

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'))
    }
  }
...
Enter fullscreen mode Exit fullscreen mode

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'))
    }
  }
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 }
},
Enter fullscreen mode Exit fullscreen mode

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()
  }
})
Enter fullscreen mode Exit fullscreen mode

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.


Experience your Vue apps exactly how a user does

Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Vue apps - Start monitoring for free.


The post Handling authentication in your GraphQL powered Vue app appeared first on LogRocket Blog.

Top comments (0)