Guides
Guide

How to Implement Passwordless Auth in Vue.js with Magic

2021-02-16
Download this example and get started in seconds:
npx make-magic --template hello-world-vue

This tutorial demonstrates how to add passwordless login to a Vue.js application using Magic Link.

Boostrap Project

CLI Quickstart Tool

To start, run the following CLI command in your terminal. The make-magic NPM package is the quickest way to bootstrap a Magic project from a list of pre-built templates.

npx make-magic --template hello-world-vue

Set up Project Name

After a few seconds, you will be prompted for a project name, this will also be the name of the directory that will be created for this project.

Magic Publishable API Key

After putting in a project name, you will be prompted for your Magic Publishable API Key, which enables user authentication with Magic.

To get your publishable API key, you'll need to sign up to Magic Dashboard. Once you've signed up, an app will be created upon your first login (you'll be able to create new apps later).

You'll now be able to see your Publishable API Key - copy and paste the key into your CLI prompt.

Select NPM Client

After hitting Enter, you'll be asked to select whether you’d like to use npm / yarn as the NPM client for your project.

Open Application

After selecting your NPM client, the vue server will automatically start, and your application will be running on http://localhost:8080.

In this example app, you'll be prompted to sign up for a new account using an email address or login to an existing one. The authentication process is secured by Magic.

After clicking on your magic link email, you'll be successfully logged in, and redirected to the profile page that displays your email and public address.

Live Demo

https://vue-magic.vercel.app

Prerequisites

Create a Sample Application

info

The following tutorial creates a new Vue application using the Vue CLI and presents some common ways to build Vue applications regarding its structure and naming conventions. If you are using this guide to integrate Magic SDK into your Vue application, you may need to adjust some of the steps to suit your needs.

If you don’t already have an existing application, you can create one using the Vue CLI tool. Using the terminal, find a location on your drive where you want to create the app and run the following commands:

# Install the Vue CLI
npm install -g @vue/cli

# Create the application using the Vue CLI.
# When asked to pick a preset,
# use your arrow key to highlight 
# Manually select features and press enter
vue create magic-vue

We will be selecting the Vue Router, Vuex, and other tools necessary for the project setup. Use spacebar to choose Vuex and Router and press enter.

Choose 2.x for the Vue.js version for this project.

Type Y for selecting the history mode for the router.

The DOM Window object provides access to the browser's session history through the history object. It exposes useful methods and properties that let you navigate back and forth through the user's history and manipulate the contents of the history stack.

To learn, read here.

Select ESLint + Prettier, but it’s optional and up to the developer’s preferences.

Select Lint on save.

And select In dedicated config files for config for Babel, ESlint, etc.

And at last, select Y or N based on your preferences for saving the presets.

It will take some time to download. Once downloaded, you will see the following screen:

Start the application

Let’s move into the project directory and start the Vue application.

cd magic-vue
npm run serve

We have added Vue Router and Vuex for this Vue.js application. And as a result, you can see an About route added. But to see Vuex in action. Let’s wait for the moment before we go ahead and explain and add Magic for our passwordless authentication to see it in action.

Add Routes

Let’s add two more routes, one for the profile and the other for the login.

Create a Profile.vue file under the src/views directory and add the following code:

<template>
 <div class="profile">
   <img alt="Vue logo" src="../assets/logo.png" />
   <h1>Welcome Shahbaz!</h1>
 </div>
</template>
<script>
export default {}
</script>
<style scoped></style>

Also, create a Login.vue file under src/views directory and add the following code:

<template>
 <div class="container">
   <div class="login">
     <form @submit.prevent="login">
       <h3>Login</h3>
       <label>
         <input
           v-model="email"
           type="email"
           name="email"
           placeholder="Email"
           required
           class="email-input email-extra"
           value
         />
       </label>
       <div class="submit">
         <button type="submit" name="button">
           Send Magic Link
         </button>
       </div>
     </form>
   </div>
 </div>
</template>
<script>
export default {
 data() {
   return {
     email: '',
   }
 },
 methods: {
   login() {
     console.log(this.email)
   },
 },
}
</script>
<style scoped>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300&display=swap');
*,
*::before,
*::after {
 box-sizing: border-box;
 font-family: 'Inter', sans-serif;
 outline: none;
}
body {
 margin: 0;
 color: #333;
 background-color: #fff;
 min-height: 100%;
}
form,
label {
 display: flex;
 flex-flow: column;
 text-align: center;
}
.email-input {
 padding: 10px;
 margin: 1rem auto;
 border: 1px solid #ccc;
 border-radius: 50px;
 outline: none;
 transition: 0.5s;
 width: 80%;
}
.email-input:focus {
 border: 1px solid #42b983;
}
.submit {
 display: flex;
 justify-content: flex-end;
 align-items: center;
 justify-content: space-between;
}
.submit > a {
 text-decoration: none;
}
.submit > button {
 padding: 0.6rem 1rem;
 cursor: pointer;
 background: #fff;
 border: 1px solid #ccc;
 border-radius: 50px;
 width: 80%;
 outline: none;
 transition: 0.3s;
 margin: 0 auto;
 font-size: 14px;
}
.submit > button:hover {
 border-color: #42b983;
}
.error {
 color: brown;
 margin: 1rem 0 0;
}
.container {
 max-width: 42rem;
 margin: 0 auto;
 padding: 2rem 1.25rem;
 overflow: auto;
}
.login {
 max-width: 20rem;
 margin: 0px auto;
 padding: 1rem;
 border: 1px solid rgb(204, 204, 204);
 border-radius: 4px;
 text-align: center;
}
</style>

We have added two routes, the login route displays a login form which we will use to sign in with Magic’s passwordless feature, and the profile route will display the user data received from Magic after a successful login. But for now, it says, Welcome Shahbaz!

Feel free to change this name 😄.

To see these two routes in action, let’s update the route file and App.vue to see the changes on the homepage.

Update the src/router/index.js file to include the following objects into the routes array:

{
   path: '/profile',
   name: 'Profile',
   component: () =>
     import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
 },
 {
   path: '/login',
   name: 'Login',
   component: () =>
     import(/* webpackChunkName: "login" */ '../views/Login.vue'),
 },

Update App.vue file to include new routes for /profile and /login and a little bit more css to nav a:

<template>
 <div id="app">
   <div id="nav">
     <router-link to="/">Home</router-link>
     <router-link to="/about">About</router-link>
     <router-link to="/profile">Profile</router-link>
     <router-link to="/login">Login</router-link>
   </div>
   <router-view />
 </div>
</template>

<style>
/*  */

#nav a {
 font-weight: bold;
 padding: 5px;
 text-decoration: none;
 color: #2c3e50;
}

</style>

Your application should look like this by now:

Install Magic SDK

After creating a new Vue application and updating the routes, install the Magic SDK for passwordless authentication.

npm install --save magic-sdk

Our login page looks like this. The email entered will be shown in the console.

Let’s add Magic to this application to allow user authentication.

note

Magic could be implemented in a single route Vue application, but that’s not how the real world example would look like, so we decided to work with Vuex, a state management library, to store the user’s state and fetch those data in different routes.

To do so, we will use Vuex, a state management library for Vue.js applications. It serves as a centralized store for all the components in an application.

To learn more about Vuex, visit the official documentation page.

Update Login.vue file

Change the <script> tag with the following code:

<script>
import { SDKError, RPCError, ExtensionError } from 'magic-sdk'

export default {
 data() {
   return {
     email: '',
   }
 },
 methods: {
   login() {
     this.$store
       .dispatch('login', {
         email: this.email,
       })
       .then(() => {
         this.$router.push({ name: 'Profile' })
       })
       .catch((err) => {
         if (err instanceof SDKError) {
           console.log(err)
         }
         if (err instanceof RPCError) {
           console.log(err)
         }
         if (err instanceof ExtensionError) {
           console.log(err)
         }
       })
   },
 },
}
</script>
  • We are using the Vuex store to dispatch a login action with email as a data object.
  • Upon successful login, it will be routed to the profile page.
  • We are also using Magic’s built-in error classes to log errors.

To learn more about Errors & Warnings, read here.

Create login action

Open the store/index.js file and replace it with the following code:

import Vue from 'vue';
import Vuex from 'vuex';
import { Magic } from 'magic-sdk';

Vue.use(Vuex);

const m = new Magic(process.env.VUE_APP_MAGIC_KEY);

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    SET_USER_DATA(state, userData) {
      state.user = userData;
      localStorage.setItem('user', JSON.stringify(userData));
    },
  },
  actions: {
    async login({ commit }, email) {
      await m.auth.loginWithMagicLink(email);
      const data = await m.user.getMetadata();
      commit('SET_USER_DATA', data);
    },
  },
  modules: {},
});

Login action is committing SET_USER_DATA mutation, which changes the user state.

We are also using localStorage to store our user object, which we will use later in this guide to auto-login our users.

Create a .env file

Create a .env file at the root of your project.

VUE_APP_MAGIC_KEY=pk_live_CC6XXXXXXX576

To get the VUE_APP_MAGIC_KEY, visit the Magic dashboard, create a new app and grab its Publishable Key.

note

You may have to stop the server and restart to use the .env file’s data.

Login with Magic

Visit the login page, enter your email address and click on Send Magic Link.

You will see something like this with your email address on the pop-up modal.

Now, check your email and click on Log in to {Application Name}.

note

The link will expire in 10 minutes if not used.

After successful login, you will see the following screen:

Let’s ReVue

After a successful login, you will be redirected to the profile page. And it looks the same as it did before because there was no change in the UI.

note

To get the most out of Vue development, please install Vue’s official devtools extension.

To see the state changes, open your browser developer tool and select Vue from the tab panel, and click on Vuex.

As you can see, our Log In action was able to commit SET_USER_DATA. This means we have successfully logged in using Magic.

note

When you refresh the page, your state object is lost. We will fix this later in the guide using created() lifecycle hook.

Update Profile.vue file

Since we store our user in state, we can use this state object to bring the user object into the Profile page.

Replace Profile.vue with the following code:

<template>
 <div class="profile">
   <img alt="Vue logo" src="../assets/logo.png" />
   <h1>Welcome {{ email }}</h1>
   <h3>
     Your Public Address is: <i>{{ publicAddress }}</i>
   </h3>
 </div>
</template>
<script>
import { mapState } from 'vuex'

export default {
 computed: mapState({
   email: (state) => state.user.email,
   issuer: (state) => state.user.issuer,
   publicAddress: (state) => state.user.publicAddress,
 }),
}
</script>
<style scoped>
h3 {
 margin: 20px 0 0;
}
</style>

Our profile page is displaying the user information.

Everything is working fine, but as you can see, we still see the login link on our navbar.

Let’s fix this by adding a logout button and hiding the profile link when the user is not logged in.

Replace App.vue to add Logout

Replace the App.vue file with the following code:

<template>
 <div id="app">
   <div id="nav">
     <router-link to="/">Home</router-link>
     <router-link to="/about">About</router-link>
     <router-link v-if="this.$store.state.user" to="/profile"
       >Profile</router-link
     >
     <router-link v-if="!this.$store.state.user" to="/login"
       >Login</router-link
     >
     <button v-else type="button" class="button" @click="logout">
       Logout
     </button>
   </div>
   <router-view />
 </div>
</template>
<script>
export default {
 methods: {
   logout() {
     this.$store.dispatch('logout')
   },
 },
}
</script>
<style>
#app {
 font-family: Avenir, Helvetica, Arial, sans-serif;
 -webkit-font-smoothing: antialiased;
 -moz-osx-font-smoothing: grayscale;
 text-align: center;
 color: #2c3e50;
}

#nav {
 padding: 30px;
}

#nav a {
 font-weight: bold;
 padding: 5px;
 text-decoration: none;
 color: #2c3e50;
}

#nav a.router-link-exact-active {
 color: #42b983;
}

#nav > button {
 padding: 0.6rem 1rem;
 cursor: pointer;
 background: #fff;
 border: 1px solid #ccc;
 border-radius: 50px;
 width: 5rem;
 outline: none;
 transition: 0.3s;
 margin: 0 auto;
 font-size: 14px;
 font-weight: bold;
}

#nav > button:hover {
 background-color: #42b983;
 color: white;
}
</style>
  • The above code adds a logout button when clicked dispatches a logout action.
  • Uses user state to show the routes

Note: the profile is still unguarded, which means anyone can view it. To fix this, we will use beforeEach guard and Route Meta Field later in this guide.

Create logout action

Open store/index.js and replace it with the following code:

import Vue from 'vue';
import Vuex from 'vuex';
import { Magic } from 'magic-sdk';

Vue.use(Vuex);

const m = new Magic(process.env.VUE_APP_MAGIC_KEY);

export default new Vuex.Store({
  state: {
    user: null,
  },
  mutations: {
    SET_USER_DATA(state, userData) {
      state.user = userData;
      localStorage.setItem('user', JSON.stringify(userData));
    },
    CLEAR_USER_DATA() {
      localStorage.removeItem('user');
      location.reload();
    },
  },
  actions: {
    async login({ commit }, email) {
      await m.auth.loginWithMagicLink(email);
      const data = await m.user.getMetadata();
      commit('SET_USER_DATA', data);
    },
    async logout({ commit }) {
      await m.user.logout();
      commit('CLEAR_USER_DATA');
    },
  },
  modules: {},
});
  • logout action calls Magic’s logout function and commits CLEAR_USER_DATA mutation
  • CLEAR_USER_DATA mutation removes the user object from the localStorage and reloads the page, which refreshes the Vue application.
  • Clicking logout doesn’t redirect the application to the home page. To fix this, let’s add Navigation Guard and a Route Meta Field.

beforeEach Navigation Guard and Route Meta Field

Replace the router/index.js file with the following code:

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
    meta: { requiresAuth: true },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue'),
  },
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes,
});

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user');

  if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
    next('/');
  }
  next();
});

export default router;

Vue router supports meta fields, which we can include when defining a route. Let’s update the /profile route object to include meta: { requiresAuth: true }:

{
  path: '/profile',
  name: 'Profile',
  component: Profile,
  meta: { requiresAuth: true }
}

Just keep in mind that meta property expects an object, and we define requireAuth: true. With the meta fields, we can create advanced route login for our needs. For instance, to prevent an action based on permission or by requiring authentication.

router.beforeEach((to, from, next) => {
  const loggedIn = localStorage.getItem('user');

  if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
    next('/');
  }
  next();
});

We also added the navigation guard with router.beforeEach(), which gets the user’s logged in state from the localStorage and checks for a record in a route object for a meta field of requireAuth.

As the name suggests, the navigation guards provided by vue-router are primarily used to guard navigations by redirecting or canceling them.

To learn more about beforeEach guard, read here.

Auto-login using created() lifecycle hook

Open your main.js file and replace it with the following code:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  created() {
    const userString = localStorage.getItem('user');
    if (userString) {
      const userData = JSON.parse(userString);
      this.$store.commit('SET_USER_DATA', userData);
    }
  },
  render: h => h(App),
}).$mount('#app');

Now, when you refresh the page, your state object won’t be lost!

Done

Our application is ready now.

Let’s ReVue:

  • We have used Vue CLI to generate a Vue.js Application
  • We have used Vue Router and Vuex to manage routes and states.
  • We have used Magic SDK to add passwordless authentication to a Vue.js application.
  • Learned about the beforeEach Navigation Guard and Route Meta Field
  • Used created() lifecycle hook to allow auto-login upon page refresh using localStorage.

Deploy to Vercel

Click here to deploy with Vercel.

Let's make some magic!