Guides
Guide

Add Magic's Passwordless Authentication in a Vue 3 Application

2021-11-16

Introduction

We’re in the 21st century, and passwords are not here to stay. Why do your users have to remember their passwords to log into your application when it is stressful? Could it be a potential security threat?

This guide will go over how we can secure our applications by implementing passwordless authentication using Vue 3 and Magic.

Prerequisites

This guide assumes that the reader has the following:

Working knowledge of JavaScript and Vue is strongly recommended.

You can install Vue CLI with the following command:

yarn global add @vue/cli
# OR
npm install -g @vue/cli

Set up Vue Application

If you don’t have an existing Vue application to start, we can bootstrap a new Vue project using the Vue CLI.

Open up the terminal and type in the following command to bootstrap a new Vue project.

vue create magic-vue-auth

You’ll be prompted to choose a preset; select the option to manually select features.

Once there, select Router and Vuex and click Enter.

Next, choose Vue version 3.x, as we’ll be using the new composition API.

Finally, click Enter on all other selections to get your Vue app ready.

Now, change the directory to the project folder with the following command:

cd magic-vue-auth

Then, start the project like so:

yarn serve
# OR
npm run serve

You can visit the application running on https://localhost:8080.

Creating our Views

Views are the various pages that are served to users when they visit our application. 

For the purpose of this guide, we’ll create just two pages; Login.vue to handle logging in a user and Dashboard.vue to fetch and display the logged-in user’s information.

Create the Login.vue file in the src/views directory and put in the following code:

<!-- Login.vue -->
<template>
  <div class="login-container">
    <form @submit.prevent="login" class="login-form">
      <h2>Log In</h2>
      <div class="email-container">
        <label>Email address</label>
        <input
          v-model="email"
          type="email"
          name="email"
          placeholder="hello@magic.link"
          required
          class="email-input"
          value
        />
      </div>
      <button type="submit" name="button">Send Magic Link</button>
    </form>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const email = ref("");
    const login = () => {
      console.log("User logged in");
    };
    return {
      email,
      login,
    };
  },
};
</script>

<style scoped>
.login-form,
.email-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.email-container {
  margin-bottom: 30px;
}
.email-container label {
  margin-right: auto;
  margin-bottom: 7px;
  font-size: 13px;
}
.email-container .email-input {
  width: 300px;
  height: 30px;
  border-radius: 5px;
  border: 1px solid gray;
  outline: none;
  padding: 0 5px;
}
.email-input:focus {
  border: 1px solid #0228af;
}
</style>

Next, create the Dashboard.vue file also in the src/views directory and copy and paste in the following code:

<!-- Dashboard.vue -->
<template>
  <div class="dashboard-container">
    <h2>Welcome to our Dashboard Page</h2>
    <p>Hello!</p>
    <button @click="logout">Sign out</button>
  </div>
</template>
<script>
export default {
  setup(){
     const logout = () => {
     console.log("User has been logged out")
    };
    return {
      logout,
    };
  }
}
</script>
<style scoped>
</style>

Setting up routes with Vue Router

Our pages have been created, now we have to set up routes that’ll enable our users to move seamlessly between the pages we just created. We’ll use Vue Router to achieve this functionality.

We need to declare routes for our newly created pages in our router configuration file which is located in the router/index.js directory.

Update the routes array in the router configuration file by adding the following route object definitions:

// router/index.js
const routes = [
  ...
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ 
    '../views/Dashboard.vue'),
    meta: { requiresAuth: true },
  },
]

The meta object in the dashboard route definition is used to hold extra information about that route. It has a property named requiresAuth which is set to true, and we’re going to use this property to guard this route against unauthenticated users.

Navigation Guards

According to the Vue docs:

note

As the name suggests, the navigation guards provided by vue-router are primarily used to guard navigations either by redirecting it or canceling it. There are a number of ways to hook into the route navigation process: globally, per-route, or in-component.

Basically, navigation guards provide a way for us to prevent certain routes from being accessed based on certain conditions.

To add navigation guards to our router configuration, import our Vuex configuration file to have access to the Vuex store, and then add the router.beforeEach function at the end of the router/index.js file to define our navigation guard.:

// router/index.js
...

import store from "../store"

...

router.beforeEach((to, from, next) => {
  const loggedIn = store.state.user;
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth && !loggedIn) {
    next('/login');
  }
  next();
})

export default router

router.beforeEach is a global navigation guard which gets the state of the logged-in user from the Vuex store and checks each of our route definitions if they contain the meta object and the requiresAuth property with the value of true.

If the current route the user is on requires authentication and the user is not logged in, they would be redirected to the login route.

You can learn more about Vue’s navigation guards here.

Let’s update our App.vue file by adding navigation links and global styles for the newly created Dashboard.vue and Login.vue pages:

<!-- App.vue -->
<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link to="/login">Login</router-link> |
    <router-link to="/dashboard">Dashboard</router-link>
  </div>
  <router-view/>
</template>
<style>
/* ... */

button {
  width: 300px;
  height: 30px;
  padding: 0 5px;
  background-color: #35aa58;
  border: none;
  border-radius: 5px;
  color: #fff;
  cursor: pointer;
}
button:hover {
  background-color: #35aa58b7;
}
</style>

Here’s what our application looks like so far:

Magic SDK Setup

The Magic SDK is what drives the entire authentication functionality for our application. We can enable this functionality by installing it:

yarn add magic-sdk
#OR
npm install --save magic-sdk

Create .env.local File

Let’s create a .env.local file at the root folder of our project. We’ll use this file to store our Magic Publishable API key so we don’t expose them in our code to anyone.

To get your Magic Publishable API key, log into your Magic dashboard and create a new application, and then on your dashboard home page, open the newly created application:

Then copy your Publishable API key:

Once you’ve copied your API key, go back to the .env.local file we created in our Vue application and set the API equal to a variable name as such:

VUE_APP_MAGIC_API_KEY=pk_live_D27xxxxxxxxxx

Setting up Vuex

Vuex is a state management tool for Vue, used to store data that needs to be made accessible to all components across our application. We’ll use Vuex to store all our application’s data and logic to use across all our components.

The thing is, Vuex has the problem of data persistence. Data stored in Vuex does not survive if the browser window is refreshed. To solve this problem, we’ll use vuex-persistedstate. This package helps save data stored in Vuex even when the browser has been refreshed.

To install vuex-persistedstate, type in the following command in the terminal:

yarn add vuex-persistedstate
#OR
npm install --save vuex-persistedstate

Configuring Vuex store

Here, we’ll import and set up Magic SDK, Vue router, and vuex-persistedstate.

Replace the code in store/index.js file directory with the following code:

// store/index.js
import { createStore } from "vuex"
import createPersistedState from "vuex-persistedstate";
import router from '../router';
import { Magic, SDKError, RPCError, ExtensionError } from 'magic-sdk';

const magicKey = new Magic(process.env.VUE_APP_MAGIC_API_KEY);

export default createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  },
  plugins: [createPersistedState()],
})

Vuex state 

The state object in our Vuex store, as the name implies stores the state of a particular piece of data in our application. We can define default values of our data here as such:

state: {
  user: null
};

In our state object, we set the default value of the user to null, this is the value as long as a user has not signed in yet.

Vuex mutations

It is not advisable to update data contained in our Vuex store without the use of mutations. This is so that Vuex can easily keep track of changes made to our application’s store.

A mutation takes in the state and a value from the action committing it like so:

mutations: {
    setUser(state, userData) {
      state.user = userData;
    },
  },

Whenever this mutation is committed, the value of user in our store’s state changes from the default value to the value being passed to it.

Vuex actions

Vuex actions are functions used to commit mutations which in turn change the state in our application.

We’ll create a login action that logs users in and a logout action that logs users out of the application.

Login Action

The login action takes in the user’s email and passes this data into Magic’s loginWithMagicLink function to be validated. It creates a new user if that email doesn’t already exist in the database, and then logs them into the application. It should also log in existing users.

The code for our login action is as follows:

actions: {
    async login({ commit }, email) {
      try {
        await magicKey.auth.loginWithMagicLink(email);
        const data = await magicKey.user.getMetadata();
        commit('setUser', data);
        await router.push({ name: 'Dashboard' })
      }
      catch (error) {
        if (error instanceof SDKError) {
          console.log(error)
        }
        if (error instanceof RPCError) {
          console.log(error)
        }
        if (error instanceof ExtensionError) {
          console.log(error)
        }
      }
    }
  },

When the login action is triggered, the magicKey.auth.loginWithMagicLink function is invoked and if successful, the data received from the function call is stored in the data variable. Next, our setUser mutation is committed and the user is finally routed to the dashboard page.

If unsuccessful, we should get an error message in the console. You can learn more about Magic’s errors and warnings here.

Logout Action

We’ll create this action to enable our users to sign out of our application.

The code for our logout action is as follows:

actions: {
  ...
   async logout({ commit }) {
      await magicKey.user.logout();
      commit('setUser', null);
      await router.push({ name: 'Home' })
    },
}

When the logout action is triggered, the magicKey.user.logout function is called and then we commit the setUser mutation to update the user state with the value of null.

Add Component Logic

We’ve created both the login and logout actions in Vuex which is basically all we’ll need for this simple app. What’s left now is to add our Vuex logic to our previously created components to enable users to log in and out of our application.

Let’s start by replacing the existing script tag of the Login.vue component with the following code.

<!-- Login.vue -->
<script>
import { ref } from "vue";
// import Vuex store
import { useStore } from "vuex";

export default {
  setup() {
    // create new store instance
    const store = useStore();
    const email = ref("");
    // dispatch the signup action to log in the user
     const login = () => {
      store.dispatch("login", {
        email: email.value
      });
    };
    return {
      email,
      login,
    };
  },
};
</script>

Now, when the user hits the login button, the login method is triggered which in turn triggers the login action in the Vuex store.

Next, let’s replace the existing code our Dashboard.vue component with the following code:

<!-- Dashboard.vue -->
<template>
  <div class="dashboard-container">
    <h2>Welcome to our Dashboard Page</h2>
    <!-- use the value of data in the template -->
    <p>Hello {{user.email}}!</p>
    <p>{{user.publicAddress}}</p>
    <button @click="logout">Sign out</button>
  </div>
</template>

<script>
// Import Vuex and Computed
import { useStore } from "vuex";
import { computed } from "vue";

export default {
  setup() {
    // create store instance
    const store = useStore();
    // fetch the value of logged in user from the Vuex store
    const user = computed(() => store.state.user);
    const logout = () => {
      // dispatch the logout action to logout user
      store.dispatch("logout");
    };
    return {
      logout,
      user
    };
  },
};
</script>

<style scoped>
</style>

In our Dashboard.vue component the template code has been updated to extract the logged-in user’s email and public address from the Vuex store. 

Here’s what our component looks like:

Then, let’s update the code in our App.vue component to replace the navigation bar depending on when a user is logged in or not. Replace the contents of the template tag with the following code, and add the new script section.

<!-- App.vue -->
<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link> |
    <router-link v-if="!user" to="/login">Login</router-link> |
    <router-link to="/dashboard">Dashboard</router-link>
  </div>
  <router-view/>
</template>

<script>
import { useStore } from "vuex";
import { computed } from "vue";

export default {
  setup() {
     const store = useStore();
    const user = computed(() => store.state.user);
    return{
      user
    }
  },
}
</script>

Here, we import the Vuex store and a computed property to check for the user’s status and if the user is logged in, the login navigation link does not show.

Conclusion

Congratulations 🎉 We’ve reached the end of the tutorial! Let's understand what we have covered.

In this guide, we’ve gone through how to:

  • implement authentication using Magic and Vue js 3.
  • manage state using Vuex
  • handle routing using Vue Router
  • persist logged in user’s data
  • create navigation guards

Let's make some magic!