Add Magic's Passwordless Authentication in a Vue 3 Application
This guide is a guest post by Iniubong Obonguko, as part of our Guest Author program.
#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:
- Yarn / NPM installed on their machine
- Vue CLI 4.5.11 installed
- Vue 3.2.0
- Vue Router 4 and Vuex 3
- A Magic Account
Working knowledge of JavaScript and Vue is strongly recommended.
You can install Vue CLI with the following command:
01
02yarn global add @vue/cli
03# OR
04npm 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.
01vue 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:
01cd magic-vue-auth
Then, start the project like so:
01
02yarn serve
03# OR
04npm 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:
01
02<!-- Login.vue -->
03<template>
04 <div class="login-container">
05 <form @submit.prevent="login" class="login-form">
06 <h2>Log In</h2>
07 <div class="email-container">
08 <label>Email address</label>
09 <input
10 v-model="email"
11 type="email"
12 name="email"
13 placeholder="hello@magic.link"
14 required
15 class="email-input"
16 value
17 />
18 </div>
19 <button type="submit" name="button">Send Magic Link</button>
20 </form>
21 </div>
22</template>
23<script>
24import { ref } from "vue";
25export default {
26 setup() {
27 const email = ref("");
28 const login = () => {
29 console.log("User logged in");
30 };
31 return {
32 email,
33 login,
34 };
35 },
36};
37</script>
38
39<style scoped>
40.login-form,
41.email-container {
42 display: flex;
43 flex-direction: column;
44 align-items: center;
45 justify-content: center;
46}
47.email-container {
48 margin-bottom: 30px;
49}
50.email-container label {
51 margin-right: auto;
52 margin-bottom: 7px;
53 font-size: 13px;
54}
55.email-container .email-input {
56 width: 300px;
57 height: 30px;
58 border-radius: 5px;
59 border: 1px solid gray;
60 outline: none;
61 padding: 0 5px;
62}
63.email-input:focus {
64 border: 1px solid #0228af;
65}
66</style>
Next, create the Dashboard.vue
file also in the src/views
directory and copy and paste in the following code:
01
02<!-- Dashboard.vue -->
03<template>
04 <div class="dashboard-container">
05 <h2>Welcome to our Dashboard Page</h2>
06 <p>Hello!</p>
07 <button @click="logout">Sign out</button>
08 </div>
09</template>
10<script>
11export default {
12 setup(){
13 const logout = () => {
14 console.log("User has been logged out")
15 };
16 return {
17 logout,
18 };
19 }
20}
21</script>
22<style scoped>
23</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:
01
02// router/index.js
03const routes = [
04 ...
05 {
06 path: '/login',
07 name: 'Login',
08 component: () => import(/* webpackChunkName: "login" */ '../views/Login.vue')
09 },
10 {
11 path: '/dashboard',
12 name: 'Dashboard',
13 component: () => import(/* webpackChunkName: "dashboard" */
14 '../views/Dashboard.vue'),
15 meta: { requiresAuth: true },
16 },
17]
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:
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.:
01
02// router/index.js
03...
04
05import store from "../store"
06
07...
08
09router.beforeEach((to, from, next) => {
10 const loggedIn = store.state.user;
11 const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
12 if (requiresAuth && !loggedIn) {
13 next('/login');
14 }
15 next();
16})
17
18export 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:
01
02<!-- App.vue -->
03<template>
04 <div id="nav">
05 <router-link to="/">Home</router-link> |
06 <router-link to="/about">About</router-link> |
07 <router-link to="/login">Login</router-link> |
08 <router-link to="/dashboard">Dashboard</router-link>
09 </div>
10 <router-view/>
11</template>
12<style>
13#app {
14
15/* ... */
16
17button {
18 width: 300px;
19 height: 30px;
20 padding: 0 5px;
21 background-color: #35aa58;
22 border: none;
23 border-radius: 5px;
24 color: #fff;
25 cursor: pointer;
26}
27button:hover {
28 background-color: #35aa58b7;
29}
30</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:
01
02yarn add magic-sdk
03#OR
04npm 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:
01VUE_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:
01
02yarn add vuex-persistedstate
03#OR
04npm 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:
01
02// store/index.js
03import { createStore } from "vuex"
04import createPersistedState from "vuex-persistedstate";
05import router from '../router';
06import { Magic, SDKError, RPCError, ExtensionError } from 'magic-sdk';
07
08const magicKey = new Magic(process.env.VUE_APP_MAGIC_API_KEY);
09
10export default createStore({
11 state: {
12 },
13 mutations: {
14 },
15 actions: {
16 },
17 modules: {
18 },
19 plugins: [createPersistedState()],
20})
#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:
01state: {
02 user: null
03};
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:
01
02mutations: {
03 setUser(state, userData) {
04 state.user = userData;
05 },
06 },
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:
01
02actions: {
03 async login({ commit }, email) {
04 try {
05 await magicKey.auth.loginWithMagicLink(email);
06 const data = await magicKey.user.getMetadata();
07 commit('setUser', data);
08 await router.push({ name: 'Dashboard' })
09 }
10 catch (error) {
11 if (error instanceof SDKError) {
12 console.log(error)
13 }
14 if (error instanceof RPCError) {
15 console.log(error)
16 }
17 if (error instanceof ExtensionError) {
18 console.log(error)
19 }
20 }
21 }
22 },
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:
01
02actions: {
03 ...
04 async logout({ commit }) {
05 await magicKey.user.logout();
06 commit('setUser', null);
07 await router.push({ name: 'Home' })
08 },
09}
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.
01
02<!-- Login.vue -->
03<script>
04import { ref } from "vue";
05// import Vuex store
06import { useStore } from "vuex";
07
08export default {
09 setup() {
10 // create new store instance
11 const store = useStore();
12 const email = ref("");
13 // dispatch the signup action to log in the user
14 const login = () => {
15 store.dispatch("login", {
16 email: email.value
17 });
18 };
19 return {
20 email,
21 login,
22 };
23 },
24};
25</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:
01
02<!-- Dashboard.vue -->
03<template>
04 <div class="dashboard-container">
05 <h2>Welcome to our Dashboard Page</h2>
06 <!-- use the value of data in the template -->
07 <p>Hello {{user.email}}!</p>
08 <p>{{user.publicAddress}}</p>
09 <button @click="logout">Sign out</button>
10 </div>
11</template>
12
13<script>
14// Import Vuex and Computed
15import { useStore } from "vuex";
16import { computed } from "vue";
17
18export default {
19 setup() {
20 // create store instance
21 const store = useStore();
22 // fetch the value of logged in user from the Vuex store
23 const user = computed(() => store.state.user);
24 const logout = () => {
25 // dispatch the logout action to logout user
26 store.dispatch("logout");
27 };
28 return {
29 logout,
30 user
31 };
32 },
33};
34</script>
35
36<style scoped>
37</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.
01
02<!-- App.vue -->
03<template>
04 <div id="nav">
05 <router-link to="/">Home</router-link> |
06 <router-link to="/about">About</router-link> |
07 <router-link v-if="!user" to="/login">Login</router-link> |
08 <router-link to="/dashboard">Dashboard</router-link>
09 </div>
10 <router-view/>
11</template>
12
13<script>
14import { useStore } from "vuex";
15import { computed } from "vue";
16
17export default {
18 setup() {
19 const store = useStore();
20 const user = computed(() => store.state.user);
21 return{
22 user
23 }
24 },
25}
26</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