guides
Guide

How to Implement Passwordless Auth in Nuxt.js with Magic

Magic Staff · April 28, 2021
Download this example and get started in seconds:
npx make-magic --template nuxt

Hi 👋, in this guide you'll be learning how to add passwordless login to a Nuxt.js application using Magic Link.

#Bootstrap 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 - similar to create-nuxt-app.

01npx make-magic --template nuxt

#Setup Project Name

After a few seconds, you will be prompted for a project name, this will also be the name of the folder 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 nuxt server will automatically start, and your application will be running on http://localhost:3000.

In this example app, you'll be prompted to sign up for a new account using an email address or login into 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, issuer, and public address.

#Live Demo

https://magic-nuxtjs.vercel.app

Now that you have your Nuxt application running locally, let's dive deep into creating a Nuxt application from scratch and add Magic authentication to it.

#Create a Nuxt application

#Prerequisites

#What is Nuxt?

Nuxt.js is a higher-level framework based on Vue.js to create production-ready modern web applications. Nuxt is inspired by Next.js, which is a framework of similar purpose, based on React.js.

#create-nuxt-app

Let’s start by creating our application using create-nuxt-app.

Bash
01
02# using yarn
03yarn create nuxt-app magic-nuxtjs
04# using npx
05npx create-nuxt-app magic-nuxtjs
06# using npm
07npm init nuxt-app magic-nuxtjs

Select these when prompted, and we are all set.

These are some basic things to get you started with the Nuxt application.

Let’s move into our magic-nuxtjs directory and start our application.

01cd magic-nuxtjs
02yarn dev

You should have our Nuxt application running on http://localhost:3000.

#Build UI of the application

Let’s start by understanding what we will be building and how our UI would look.

Layout:

  • A home page to display the login state.
  • A login page to capture the user’s email address.
  • A profile page to display the user’s data.
  • A header component to include a header on all of our pages.
  • And an update to our default layout.

Firstly, we will replace the default.vue layout.

#Update default layout

Open default.vue under the layouts directory and replace it with the following code:

Javascript
01
02<template>
03 <div>
04   <Header />
05   <Nuxt />
06 </div>
07</template>
08
09<style>
10*,
11*::before,
12*::after {
13 box-sizing: border-box;
14 margin: 0;
15}
16* {
17 font-family: sans-serif !important;
18 outline: none;
19}
20.container {
21 max-width: 42rem;
22 margin: 0 auto;
23 padding: 0 10px;
24}
25</style>

This code adds the <Header /> component to our application, visible to all our pages. Also, it adds basic styling to our application.

With Nuxt.js you can create your components and auto import them into your .vue files meaning there is no need to manually import them in the script section. Nuxt.js will scan and auto import these for you once you have components set to true in your nuxt.config.js file.

#Create Header component

Now, let’s create that <Header /> component.

Create Header.vue file inside the components directory and fill it with the following code:

Javascript
01
02<template>
03
04 <header>
05   <nav>
06     <ul v-if="$store.state.authenticated">
07       <li><NuxtLink to="/">Home</NuxtLink></li>
08       <li><NuxtLink to="/profile">Profile</NuxtLink></li>
09       <li><a class="logout" @click="userLogout">Logout</a></li>
10     </ul>
11     <ul v-else>
12       <li>
13         <a class="logout">Magic Nuxt Demo</a>
14       </li>
15     </ul>
16   </nav>
17 </header>
18</template>
19
20<script>
21export default {
22 methods: {
23   async userLogout() {
24     this.$store.dispatch('logout')
25   },
26 },
27}
28</script>
29
30<style>
31nav {
32 max-width: 45rem;
33 margin: 0 auto 50px;
34 padding: 1.25rem 1.25rem;
35 border-bottom: 1px solid #f0f0f0;
36}
37ul {
38 display: flex;
39 list-style: none;
40}
41li {
42 margin-right: 1.5rem;
43 line-height: 38px;
44}
45li:first-child {
46 margin-left: auto;
47}
48a {
49 text-decoration: none;
50 color: rgba(103, 81, 255, 1);
51}
52a:hover {
53 color: rgba(103, 81, 255, 0.5);
54}
55.logout {
56 color: rgba(191, 144, 69, 1);
57 cursor: pointer;
58}
59.logout:hover {
60 color: rgba(191, 144, 69, 0.8);
61}
62</style>

In this file, we have three navigation links for our header: Home, Profile, and Logout. Home and Profile uses <NuxtLink> to navigate in between the pages. Logout calls a userLogout() function, which uses $store to dispatch a logout action. This logs the user out and then, if successful, redirects them to the login page.

We will learn more about $store later in this guide. But for now, let’s continue building our UI.

#Create Login page

Now, we will create a login page where the user can log in with their email address.

Create a login.vue file inside the pages directory, and fill it with the following code:

Javascript
01
02<template>
03 <div class="container">
04   <div class="login">
05     <form @submit.prevent="userLogin">
06       <h3 class="form-header">Login</h3>
07       <div class="input-wrapper">
08         <input
09           v-model="email"
10           class="input-email"
11           type="email"
12           placeholder="Enter your email"
13           required
14         />
15       </div>
16       <div class="btn-wrapper">
17         <button type="submit" class="btn-submit">Send Magic Link</button>
18       </div>
19     </form>
20   </div>
21 </div>
22</template>
23
24<script>
25export default {
26 data() {
27   return {
28     email: '',
29   }
30 },
31 mounted() {
32   // If the user is authenticated, redirect to profile page.
33   if (this.$store.state.authenticated) {
34     this.$router.push('/profile')
35   }
36 },
37 methods: {
38   async userLogin() {
39     this.$store
40       .dispatch('login', {
41         email: this.email,
42       })
43       .then(() => {
44         this.$router.push('/profile')
45       })
46       .catch((err) => {
47         console.log(err)
48       })
49   },
50 },
51}
52</script>
53
54<style>
55.login {
56 max-width: 20rem;
57 margin: 40px auto 0;
58 padding: 1rem;
59 border: 1px solid #dfe1e5;
60 border-radius: 4px;
61 text-align: center;
62 box-shadow: 0px 0px 6px 6px #f7f7f7;
63 box-sizing: border-box;
64}
65form,
66label {
67 display: flex;
68 flex-flow: column;
69 text-align: center;
70}
71.form-header {
72 font-size: 22px;
73 margin: 25px 0;
74 font-weight: 500;
75}
76.input-wrapper {
77 width: 80%;
78 margin: 0 auto 20px;
79}
80.input-email {
81 padding: 7px 16px;
82 border-radius: 10px;
83 border: 1px solid #000;
84 font-size: 16px;
85 font-weight: 400;
86}
87.input-email:focus,
88.input-email:hover {
89 border: 1px solid #6851ff;
90}
91.btn-wrapper {
92 margin: 0px auto 25px;
93}
94.btn-submit {
95 padding: 6px 24px;
96 border: 1px solid transparent;
97 border-radius: 2147483647px;
98 background-color: rgba(103, 81, 255, 1);
99 color: #fff;
100 cursor: pointer;
101 outline: none;
102 font-size: 16px;
103 font-weight: 400;
104}
105</style>

In this file, we have a form with an email field and a Send Magic Link button. When the button is clicked, it triggers the userLogin function, which uses $store to dispatch a login action with the user’s email as a data object.

We will learn more about $store later in the guide. But for now, assume the login has happened successfully, and that the page is redirected to the /profile route.

#Create Profile page

Let’s create our profile page to display the user’s data after a successful login.

Create a profile.vue file inside the pages directory, and fill it with the following code:

Javascript
01
02<template>
03 <div class="container">
04   <div class="label">Email</div>
05   <div class="profile-info">{{ $store.state.user.email }}</div>
06
07   <div class="label">Issuer</div>
08   <div class="profile-info">{{ $store.state.user.issuer }}</div>
09
10   <div class="label">Public Address</div>
11   <div class="profile-info">{{ $store.state.user.publicAddress }}</div>
12 </div>
13</template>
14
15<script>
16export default {
17 middleware: 'magicauth',
18}
19</script>
20
21<style>
22.label {
23 font-size: 12px;
24 color: #6851ff;
25 margin: 30px 0 5px;
26}
27.profile-info {
28 font-size: 17px;
29 word-wrap: break-word;
30}
31</style>

This page displays the Email, Issuer, and Public Address of an authenticated user. Here, the Issuer is the user’s unique ID given by Magic. It uses magicauth middleware to protect this page from unauthorized users.

We will learn more about the magicauth middleware later in this guide.

#Update Home page

Update our home page and protect it from unauthorized users.

Open index.vue file under the pages directory and replace it with the following code:

Javascript
01
02<template>
03
04 <div class="container">
05   <div>You are logged in!</div>
06 </div>
07</template>
08
09<script>
10export default {
11 middleware: 'magicauth',
12}
13</script>

#Add Magic Authentication

Magic is a plug-and-play SDK that enables passwordless authentication using magic links.

#Install Magic

01yarn add magic-sdk

#Create .env file

Create a .env file at the root of your application and add NUXT_ENV_MAGIC_PUBLISHABLE_KEY to it.

01NUXT_ENV_MAGIC_PUBLISHABLE_KEY=pk_live_abc

Sign Up with Magic and get your NUXT_ENV_MAGIC_PUBLISHABLE_KEY.

Note: When moving to Production, we recommend that you whitelist your domain for your Publishable API Key on the Magic Dashboard so that your API keys only work on your domain.

#Magic Nuxt Plugin

Create a magic.js file under the plugins directory and fill it with the following code:

Javascript
01
02import { Magic } from 'magic-sdk';
03
04// Create client-side Magic instance
05const createMagic = key => {
06  return typeof window != 'undefined' && new Magic(key);
07};
08
09export const magic = createMagic(process.env.NUXT_ENV_MAGIC_PUBLISHABLE_KEY);

Here we instantiate the Magic Client SDK, which we will use in our store.

#Build Store and Middleware

#Store

Create a index.js under the store directory and fill it with the following code:

Javascript
01
02import { magic } from '../plugins/magic';
03
04export const state = () => ({
05  user: null,
06  authenticated: false,
07});
08
09export const mutations = {
10  SET_USER_DATA(state, userData) {
11    state.user = userData;
12    state.authenticated = true;
13  },
14  CLEAR_USER_DATA(state) {
15    state.user = null;
16    state.authenticated = false;
17    this.$router.push('/login');
18  },
19};
20
21export const actions = {
22  async login({ commit }, email) {
23    await magic.auth.loginWithMagicLink(email);
24    const metadata = await magic.user.getMetadata();
25    commit('SET_USER_DATA', metadata);
26  },
27  async logout({ commit }) {
28    await magic.user.logout();
29    commit('CLEAR_USER_DATA');
30  },
31};

The store directory contains the Vuex Store files. The Vuex Store comes with Nuxt.js out of the box but is disabled by default. Creating an index.js file in this directory enables the store.

We include the magic.js plugin at the top of the file. Using Vuex with Nuxt, we export the state as a function and the mutations and actions as objects.

We have two states, user and authenticated. The user stores the user’s metadata received by Magic. And, authenticated stores the state of the authenticated user.

Then, we have two mutations and two actions.

  • The login action calls Magic’s loginWithMagicLink function to authenticate the user with their email address. After that, we get the user’s metadata by calling magic.user.getMetadata() function. And commit SET_USER_DATA mutation with metadata as payload, which changes the user and authenticated state.
  • The logout action calls Magic’s logout function to log out a user from the Magic end. And commit the CLEAR_USER_DATA mutation, which removes the user object and turns authenticated to false. Then redirects the user to the login page.

#Middleware

Create a magicauth.js the under middleware directory and fill it with the following code:

Javascript
01
02export default function ({ store, redirect }) {
03  // If the user is not authenticated, redirect to login page.
04  if (!store.state.authenticated) {
05    return redirect('/login');
06  }
07}

Middleware lets you define custom functions that can be run before rendering either a page or a group of pages (layout).

We have defined a magicauth middleware, which checks the user’s authenticated state and redirects the user accordingly.

Our application is ready now. You can start the server by running yarn dev or npm run dev. Everything will work fine, but there’s a catch. When you try to reload the page, the Vuex state loses its persistent state. So, let’s fix that by persisting the State.

#Persist State

To persist the state, we are going to use the vuex-persistedstate package.

#Install vuex-persistedstate

01yarn add vuex-persistedstate @nuxtjs-axios

Note: vuex-persistedstate uses @nuxtjs/axios as its dependency. It will not work if not installed and configured properly.

#persistedState

Create a persistedState.client.js file under the plugin directory and fill it with the following code:

Javascript
01
02/*
03 * Naming your plugin 'xxx.client.js' will make it execute only on the client-side.
04 * https://nuxtjs.org/guide/plugins/#name-conventional-plugin
05 */
06import createPersistedState from 'vuex-persistedstate';
07
08export default ({ store }) => {
09  createPersistedState()(store);
10};

And we want to persist the state at the client’s side. Nuxt is both for a client and a server-side application. To learn more about what Nuxt can do, visit https://nuxtjs.org.

#Update nuxt.config.js

Open nuxt.config.js file, add @nuxtjs/axios inside the modules and persisted plugin inside the plugins:

Javascript
01
02// Modules: https://go.nuxtjs.dev/config-modules
03modules: ['@nuxtjs/axios'],
04
05plugins: [{ src: '~/plugins/persistedState.client.js' }],
06/*
07.
08.
09*/

#Test your persistent state

Let’s start the server:

Bash
01
02yarn dev

Your application should be running on http://localhost:3000 and would look like this:

Enter your email address, and click Send Magic Link. An email will come to your email address. Open that mail, click on the button that says Log in to {your-application}. Come back to the open window. You will be redirected to the profile page, which would look like this but with your details:

Try reloading the page. Your data is persisted now.

If you run into any issues, please share those in the comments below.

#Done

Congratulations! You have successfully developed a Nuxt.js application that has user authentication built with Magic.

#Deploy

#Deploy to Vercel

Click here to deploy with Vercel.

#Deploy to Netlify

Click here to deploy with Vercel.

Let's make some magic!