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
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:
- Node.js 10x or higher
- Yarn / npm 5.2 or higher installed on their PC. This tutorial would be making use of Yarn
- Basic knowledge of JavaScript and how Vue.js works
- Knowledge of how GraphQL works
- Vue CLI installed on your PC
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:
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:
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.
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:
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
:
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
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:
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.
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)