Handling Vue Authentication using GraphQL API
In this tutorial, we will be using Vuex and ApolloClient connected to a GraphQL API to handle authentication in our Vuejs app.
Authentication and Authorization
Authentication and Authorization are most often used interchangeably, but they’re different concepts entirely. Authentication identifies or verifies who a user is while Authorization is validating the routes (or parts of the app) the authenticated user can have access to. In this tutorial, we would just be dealing with local authentication. The most popular way for handling authentication in most modern apps is by using username and password. The flow for implementing this is:
- User signs up using password and email
- The user credentials are stored in a database
- When registration is successful, the user is redirected to the login
- On successful authentication, the user is granted access to specific resources
- The user state is stored in any one of the browser storage mediums (localStorage, cookies, session) or JWT.
Prerequisites
You need to have some of the following to work through this tutorial:
- Node 6 or higher (download here)
- Yarn (recommended) or NPM (install yarn here)
- Vue CLI
- GraphQL Playground app. Download here
yarn global add @vue/cli
- Knowledge of GraphQL and VueJS and State Management with Vuex
- …and a very inquisitive mind.
Before we kick off
Learn Vue.js and modern, cutting-edge front-end technologies from core-team members and industry experts with our premium tutorials and video courses on VueSchool.io.
Dependencies
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, with rules ensuring that the state can only be mutated predictably.
GraphQL – GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.
Vue-Apollo – This library integrates apollo in your Vue components with declarative queries. Apollo is a set of tools and community efforts to help you use GraphQL in your apps.
The GraphQL API
The API was built with
- Apollo Server for the server
- GraphQL Schema Definition language for the schema
- Nodejs, bcrypt and jwt were used to create the resolvers (Mutations, Queries)
- Sequelize ORM for the database relationships
- and so on…
This is the repo: Jolaolu/graphql-blog-API. This is the API deployed to Heroku: https://bloggr-api.herokuapp.com/ We would use Vue-Apollo to connect to the API very soon.
Project Setup
Open your terminal and type this commands
vue create apollo-auth && cd apollo-auth && vue add apollo
This command would create the vue project and add the apollo graphql plugins.
Choose Manually select features.(use arrow keys for navigation and spacebar to select).Select Babel, Router(vue-router), Vuex and Linter/Formatter.
cd apollo-auth // change directory to project folder
yarn serve // start the development server. You can also use npm run serve
The apollo plugin would continue installing immediately after the project installation is complete. Apollo plugin would add apollo.config.js
to the project root folder and vue-apollo to the src folder of the project. It also adds Apollo Provider in the main.js
; Apollo provider would allow our components access to the Apollo client instance.
During Apollo installation, you would be prompted to answer some questions, you can use my config or just No to everything (it would still work properly).
The Code
Let’s work on the User Interface for our app. We would need 3 views in our app;
- Dashboard,
- Login form
- SignUp form
First, let’s setup the page layout.
Go to the src
folder and open App.vue
delete and add this:
<template>
<div id="app">
<header class="header">
<div class="app-name">VueGraph Authenticator</div>
<div v-if="authStatus" id="nav">
<div> Hi {{user.name}}</div>
<button class="auth-button" @click="logOut" > Log Out</button>
</div>
</header>
<router-view/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'App',
component: {
},
data () {
return {
}
},
methods: {
logOut: function () {
this.$store.dispatch('logOut')
.then(() => this.$router.push('/login'))
}
},
computed: {
...mapGetters(['authStatus', 'user'])
}
}
</script>
<style>
#app {
font-family: 'Baloo Chettan 2', cursive;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
width: 100%;
}
#nav {
display: flex;
}
#nav>div{
color: white;
margin-top: 1rem;
margin-right: 2rem;
font-size: 2rem;
font-weight: 500;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
Let’s write functionality for login, logout and sign up.
Create Login.vue
and SignUp.vue
files in the view folder
Login.vue
<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="jagaban@borgu.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 style="color:rgb(0, 128, 255)" to="/"> Register </router-link> </p>
</form>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Login',
data () {
return {
authDetails: {
email: '',
password: ''
}
}
},
methods: {
...mapActions(['login']),
loginUser: function () {
this.login(this.authDetails)
.then(() => this.$router.push('/dashboard'))
}
}
}
</script>
In the login.vue component, we are importing vuex mapActions
and passing login
to it. Later in our Vuex store actions, we would create a login action. The loginUser()
get our authentication details from the store and if it resolves, redirects the user to the dashboard route.
SignUp.vue
<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="jagaban@borgu.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" style="color:rgb(0, 128, 255)"> Login </router-link> </p>
</form>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Register',
data () {
return {
authDetails: {
name: '',
email: '',
password: ''
}
}
},
methods: {
...mapActions(['register']),
registerUser: function () {
this.register(this.authDetails)
.then(() => this.$router.push('/dashboard'))
}
}
}
</script>
<style>
</style>
Our signup.vue , we have similar functionality with login.vue, just pulling register
from our store actions and invoking it in our registerUser
method.
Let’s create the dashboard component. Go to views and create a Home.vue
file.
Home.vue
<template>
<div class="home">
<main>
<h1> Hey {{user.name}} you have been authenticated </h1>
<div>
<h4> These are your details... </h4>
<p> Email: {{user.email}}</p>
<p> Full Name: {{user.name}}</p>
</div>
</main>
</div>
</template>
<script>
// @ is an alias to /src
import { mapGetters } from 'vuex'
export default {
name: 'Home',
components: {
},
computed: {
...mapGetters(['user'])
}
}
</script>
<style scoped>
main{
padding: 2rem;
display: grid;
place-content: center;
}
h1{
font-size: 36px;
}
</style>
Here, we are simply importing getters from our vuex
store. MapGetters simply store getters to local computed properties. In our vuex store, the getters
has our authenticated user object.
Integrating GraphQL API using Vue Apollo
In this section we integrate the authentication API and just inject the URL to vue apollo.
Firstly, let’s write queries and mutations we would use later in our vuex store.
Mutation.js
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
}
}
`
In our mutation.js
, we would export two graphql functions, LOGIN_USER
and REGISTER_USER
. Login user accepts user email and password and returns the toke, Register user accepts username, email, and password and returns a token too. Let’s try it out with GraphQL Playground.
The API endpoint is https://bloggr-api.herokuapp.com/
.
queries.js
import gql from 'graphql-tag'
export const LOGGED_IN_USER = gql`
query {
me {
id
name
email
posts{
title
}
}
}
`
Now let’s set up our Vue Apollo
You should have vue-apollo.config.js
and apollo-config.js
in your project directory, if you installed the vue-apollo plugin.
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { setContext } from 'apollo-link-context'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
// Install the vue plugin
Vue.use(VueApollo)
// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || '<https://bloggr-api.herokuapp.com/>'
// Config
const authLink = setContext(async (_, { headers }) => {
// Use your async token function here:
const token = JSON.parse(localStorage.getItem('apollo-token'))
// Return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token || ''
}
}
})
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
// Override default apollo link
// note: don't override httpLink here, specify httpLink options in the
// httpLinkOptions property of defaultOptions.
// link: myLink
link: authLink
// Override default cache
// cache: myCache
// Override the way the Authorization header is set
// getAuth: (tokenName) => { }
// Additional ApolloClient options
// apollo: { ... }
// Client local data (see apollo-link-state)
// clientState: { resolvers: { ... }, defaults: { ... } }
}
// Create apollo client
export const { apolloClient, wsClient } = createApolloClient({
...defaultOptions
// ...options
})
apolloClient.wsClient = wsClient
// Call this in the Vue app file
export function createProvider (options = {}) {
// 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
}
// Manually call this when user log in
export async function onLogin (apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message)
}
}
// Manually call this when user log out
export async function onLogout (apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
}
}
Let’s begin with setContext
import { setContext } from 'apollo-link-context'
In graphql, a context is value which is provided to every resolver and holds important contextual information like the currently logged in user, or access to a database.
The setContext()
takes a function that returns either an object or a promise that returns an object to set the new context of a request. Simply, we use it to add authorization header to our HTTP request.
const authLink = setContext(async (_, { headers }) => {
// Use your async token function here:
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 is the defaultOptions
object.
const defaultOptions = {
httpEndpoint,
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
tokenName: AUTH_TOKEN,
persisting: false,
websocketsOnly: false,
ssr: false,
link: authLink
}
We are passing the httpEndpoint
, the tokenName
, setting server-side rendering to false and overriding the apollo link.
Then we have these two functions, onLogin
and onLogOut
.
In the onLogin()
, we are passing apolloClient and token as parameters and saying that if localStorage
is not undefined and there’s a token
, it will append the token to AUTH_TOKEN (the token name in our defaultOptions object).
In the onLogout
, we are just removing the token we set in onLogin
.
export async function onLogin (apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message)
}
}
export async function onLogout (apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN)
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
}
}
Setup Routing of Views
Open the router folder and replace the code with this in index.js
.
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '../store'
Vue.use(VueRouter)
const routes = [
{
path: '/dashboard',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "login" */ '@/views/Login.vue')
},
{
path: '/',
name: 'Register',
component: () => import(/* webpackChunkName: "register" */ '@/views/Register.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// Global Route Guards
router.beforeEach((to, from, next) => {
// Check if the user is logged in
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()
}
})
export default router
Let’s now go through the moving parts in this snippet.
Firstly, we are importing our vuex store, we would need it later for accessing the getters isAuthenticated
state.
Secondly, we included a meta
field in our route to /dashboard
, we want to ensure that the user won’t be able to access /dashboard
without getting authenticated. That’s what meta: {requiresAuth: true}
is doing.
Finally, we access the meta object by iterating over $route.matched
to check for meta fields in route records. Then we get the isAuthenticated state from our getters.
State Management with VueX
Open the store folder and add this to index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { onLogout, apolloClient } from '@/vue-apollo'
import { LOGGED_IN_USER } from '@/graphql/queries'
import { LOGIN_USER, REGISTER_USER } from '@/graphql/mutations'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: null,
user: {},
authStatus: false
},
getters: {
isAuthenticated: state => !!state.token,
authStatus: state => state.authStatus,
user: state => state.user
},
mutations: {
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')
}
},
actions: {
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)
// onLogin(apolloClient, user.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')
onLogout(apolloClient)
}
}
})
Let’s go through the moving parts of the this code snippet.
Firstly, we import, onLogout
and apolloClient
from our vue-apollo config, then we get the loggedIn user query from our graphql queries and the mutations for login and register user.
import { onLogout, apolloClient } from '@/vue-apollo'
import { LOGGED_IN_USER } from '@/graphql/queries'
import { LOGIN_USER, REGISTER_USER } from '@/graphql/mutations'
Next, we define initial state values.
state: {
token: null,
user: {},
authStatus: false
},
Then, set up some useful getters:
getters: {
isAuthenticated: state => !!state.token,
authStatus: state => state.authStatus,
user: state => state.user
},
The isAuthenticated
field here is just a way to seperate data from app logic and making our getter future proof. It’s just best practice.
Next, we would write mutations:
mutations: {
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')
}
},
SET_TOKEN
mutation has token
as the payload and assigns the token to state.token
. LOGIN_USER
sets authStatus to true and LOGOUT_USER
removes the localStorage item. Pretty straight forward. New to mutations, check here.
Finally, we write our actions for register
, login
and setUser
:
actions: {
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)
// onLogin(apolloClient, user.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')
onLogout(apolloClient)
}
}
You probably already know that actions commit mutations and contain async operations. Check here.
In our register function, we pass authDetails as an argument here. AuthDetails is actually an object that has the username, email and password and we use the spread operator to get the content of that object and assign it to the variables object of our apolloClient mutation. To learn more about apolloClient mutations, check here. The login function has similar functionality.
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)
// onLogin(apolloClient, user.token)
localStorage.setItem('apollo-token', token)
dispatch('setUser')
} catch (e) {
console.log(e)
}
},
Summary
Screenshots of the working App
You can go ahead and deploy it to Netlify or any other CDN provider. The full source code is on Github. If you’re new to client-side graphql with vue, check my previous article here.