How to Implement Passwordless Auth in Vue.js with Magic
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.
01npx 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
#Prerequisites
- Vue 2.6.12
- Vue CLI 4.5.11
- Vue Router and Vuex 3
- Magic SDK
#Create a Sample Application
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:
01# Install the Vue CLI
02npm install -g @vue/cli
03
04# Create the application using the Vue CLI.
05# When asked to pick a preset,
06# use your arrow key to highlight
07# Manually select features and press enter
08vue 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.
01cd magic-vue
02npm 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:
01<template>
02 <div class="profile">
03 <img alt="Vue logo" src="../assets/logo.png" />
04 <h1>Welcome Shahbaz!</h1>
05 </div>
06</template>
07<script>
08export default {}
09</script>
10<style scoped></style>
Also, create a Login.vue
file under src/views
directory and add the following code:
01<template>
02 <div class="container">
03 <div class="login">
04 <form @submit.prevent="login">
05 <h3>Login</h3>
06 <label>
07 <input
08 v-model="email"
09 type="email"
10 name="email"
11 placeholder="Email"
12 required
13 class="email-input email-extra"
14 value
15 />
16 </label>
17 <div class="submit">
18 <button type="submit" name="button">
19 Send Magic Link
20 </button>
21 </div>
22 </form>
23 </div>
24 </div>
25</template>
26<script>
27export default {
28 data() {
29 return {
30 email: '',
31 }
32 },
33 methods: {
34 login() {
35 console.log(this.email)
36 },
37 },
38}
39</script>
40<style scoped>
41@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300&display=swap');
42*,
43*::before,
44*::after {
45 box-sizing: border-box;
46 font-family: 'Inter', sans-serif;
47 outline: none;
48}
49body {
50 margin: 0;
51 color: #333;
52 background-color: #fff;
53 min-height: 100%;
54}
55form,
56label {
57 display: flex;
58 flex-flow: column;
59 text-align: center;
60}
61.email-input {
62 padding: 10px;
63 margin: 1rem auto;
64 border: 1px solid #ccc;
65 border-radius: 50px;
66 outline: none;
67 transition: 0.5s;
68 width: 80%;
69}
70.email-input:focus {
71 border: 1px solid #42b983;
72}
73.submit {
74 display: flex;
75 justify-content: flex-end;
76 align-items: center;
77 justify-content: space-between;
78}
79.submit > a {
80 text-decoration: none;
81}
82.submit > button {
83 padding: 0.6rem 1rem;
84 cursor: pointer;
85 background: #fff;
86 border: 1px solid #ccc;
87 border-radius: 50px;
88 width: 80%;
89 outline: none;
90 transition: 0.3s;
91 margin: 0 auto;
92 font-size: 14px;
93}
94.submit > button:hover {
95 border-color: #42b983;
96}
97.error {
98 color: brown;
99 margin: 1rem 0 0;
100}
101.container {
102 max-width: 42rem;
103 margin: 0 auto;
104 padding: 2rem 1.25rem;
105 overflow: auto;
106}
107.login {
108 max-width: 20rem;
109 margin: 0px auto;
110 padding: 1rem;
111 border: 1px solid rgb(204, 204, 204);
112 border-radius: 4px;
113 text-align: center;
114}
115</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:
01{
02 path: '/profile',
03 name: 'Profile',
04 component: () =>
05 import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
06 },
07 {
08 path: '/login',
09 name: 'Login',
10 component: () =>
11 import(/* webpackChunkName: "login" */ '../views/Login.vue'),
12 },
Update App.vue
file to include new routes for /profile
and /login
and a little bit more css to nav a
:
01<template>
02 <div id="app">
03 <div id="nav">
04 <router-link to="/">Home</router-link>
05 <router-link to="/about">About</router-link>
06 <router-link to="/profile">Profile</router-link>
07 <router-link to="/login">Login</router-link>
08 </div>
09 <router-view />
10 </div>
11</template>
12
13<style>
14/* */
15
16#nav a {
17 font-weight: bold;
18 padding: 5px;
19 text-decoration: none;
20 color: #2c3e50;
21}
22
23</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.
01npm 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.
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:
01<script>
02import { SDKError, RPCError, ExtensionError } from 'magic-sdk'
03
04export default {
05 data() {
06 return {
07 email: '',
08 }
09 },
10 methods: {
11 login() {
12 this.$store
13 .dispatch('login', {
14 email: this.email,
15 })
16 .then(() => {
17 this.$router.push({ name: 'Profile' })
18 })
19 .catch((err) => {
20 if (err instanceof SDKError) {
21 console.log(err)
22 }
23 if (err instanceof RPCError) {
24 console.log(err)
25 }
26 if (err instanceof ExtensionError) {
27 console.log(err)
28 }
29 })
30 },
31 },
32}
33</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:
01import Vue from 'vue';
02import Vuex from 'vuex';
03import { Magic } from 'magic-sdk';
04
05Vue.use(Vuex);
06
07const m = new Magic(process.env.VUE_APP_MAGIC_KEY);
08
09export default new Vuex.Store({
10 state: {
11 user: null,
12 },
13 mutations: {
14 SET_USER_DATA(state, userData) {
15 state.user = userData;
16 localStorage.setItem('user', JSON.stringify(userData));
17 },
18 },
19 actions: {
20 async login({ commit }, email) {
21 await m.auth.loginWithMagicLink(email);
22 const data = await m.user.getMetadata();
23 commit('SET_USER_DATA', data);
24 },
25 },
26 modules: {},
27});
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.
01VUE_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
.
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}
.
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.
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.
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:
01<template>
02 <div class="profile">
03 <img alt="Vue logo" src="../assets/logo.png" />
04 <h1>Welcome {{ email }}</h1>
05 <h3>
06 Your Public Address is: <i>{{ publicAddress }}</i>
07 </h3>
08 </div>
09</template>
10<script>
11import { mapState } from 'vuex'
12
13export default {
14 computed: mapState({
15 email: (state) => state.user.email,
16 issuer: (state) => state.user.issuer,
17 publicAddress: (state) => state.user.publicAddress,
18 }),
19}
20</script>
21<style scoped>
22h3 {
23 margin: 20px 0 0;
24}
25</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:
01<template>
02 <div id="app">
03 <div id="nav">
04 <router-link to="/">Home</router-link>
05 <router-link to="/about">About</router-link>
06 <router-link v-if="this.$store.state.user" to="/profile"
07 >Profile</router-link
08 >
09 <router-link v-if="!this.$store.state.user" to="/login"
10 >Login</router-link
11 >
12 <button v-else type="button" class="button" @click="logout">
13 Logout
14 </button>
15 </div>
16 <router-view />
17 </div>
18</template>
19<script>
20export default {
21 methods: {
22 logout() {
23 this.$store.dispatch('logout')
24 },
25 },
26}
27</script>
28<style>
29#app {
30 font-family: Avenir, Helvetica, Arial, sans-serif;
31 -webkit-font-smoothing: antialiased;
32 -moz-osx-font-smoothing: grayscale;
33 text-align: center;
34 color: #2c3e50;
35}
36
37#nav {
38 padding: 30px;
39}
40
41#nav a {
42 font-weight: bold;
43 padding: 5px;
44 text-decoration: none;
45 color: #2c3e50;
46}
47
48#nav a.router-link-exact-active {
49 color: #42b983;
50}
51
52#nav > button {
53 padding: 0.6rem 1rem;
54 cursor: pointer;
55 background: #fff;
56 border: 1px solid #ccc;
57 border-radius: 50px;
58 width: 5rem;
59 outline: none;
60 transition: 0.3s;
61 margin: 0 auto;
62 font-size: 14px;
63 font-weight: bold;
64}
65
66#nav > button:hover {
67 background-color: #42b983;
68 color: white;
69}
70</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:
01import Vue from 'vue';
02import Vuex from 'vuex';
03import { Magic } from 'magic-sdk';
04
05Vue.use(Vuex);
06
07const m = new Magic(process.env.VUE_APP_MAGIC_KEY);
08
09export default new Vuex.Store({
10 state: {
11 user: null,
12 },
13 mutations: {
14 SET_USER_DATA(state, userData) {
15 state.user = userData;
16 localStorage.setItem('user', JSON.stringify(userData));
17 },
18 CLEAR_USER_DATA() {
19 localStorage.removeItem('user');
20 location.reload();
21 },
22 },
23 actions: {
24 async login({ commit }, email) {
25 await m.auth.loginWithMagicLink(email);
26 const data = await m.user.getMetadata();
27 commit('SET_USER_DATA', data);
28 },
29 async logout({ commit }) {
30 await m.user.logout();
31 commit('CLEAR_USER_DATA');
32 },
33 },
34 modules: {},
35});
- 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:
01import Vue from 'vue';
02import VueRouter from 'vue-router';
03import Home from '../views/Home.vue';
04
05Vue.use(VueRouter);
06
07const routes = [
08 {
09 path: '/',
10 name: 'Home',
11 component: Home,
12 },
13 {
14 path: '/about',
15 name: 'About',
16 // route level code-splitting
17 // this generates a separate chunk (about.[hash].js) for this route
18 // which is lazy-loaded when the route is visited.
19 component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
20 },
21 {
22 path: '/profile',
23 name: 'Profile',
24 component: () => import(/* webpackChunkName: "profile" */ '../views/Profile.vue'),
25 meta: { requiresAuth: true },
26 },
27 {
28 path: '/login',
29 name: 'Login',
30 component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue'),
31 },
32];
33
34const router = new VueRouter({
35 mode: 'history',
36 base: process.env.BASE_URL,
37 routes,
38});
39
40router.beforeEach((to, from, next) => {
41 const loggedIn = localStorage.getItem('user');
42
43 if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
44 next('/');
45 }
46 next();
47});
48
49export 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 }
:
01{
02 path: '/profile',
03 name: 'Profile',
04 component: Profile,
05 meta: { requiresAuth: true }
06}
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.
01router.beforeEach((to, from, next) => {
02 const loggedIn = localStorage.getItem('user');
03
04 if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) {
05 next('/');
06 }
07 next();
08});
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:
01import Vue from 'vue';
02import App from './App.vue';
03import router from './router';
04import store from './store';
05
06Vue.config.productionTip = false;
07
08new Vue({
09 router,
10 store,
11 created() {
12 const userString = localStorage.getItem('user');
13 if (userString) {
14 const userData = JSON.parse(userString);
15 this.$store.commit('SET_USER_DATA', userData);
16 }
17 },
18 render: h => h(App),
19}).$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.