Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Vue.js 2 Authentication Tutorial, Part 3

DZone's Guide to

Vue.js 2 Authentication Tutorial, Part 3

Welcome back for our third and final article of this series! Now that we've built our Vue.js application, we'll add authentication to it.

· Web Dev Zone
Free Resource

Learn how to build modern digital experience apps with Crafter CMS. Download this eBook now. Brought to you in partnership with Crafter Software

Adding Authentication to Your Vue.js 2 App

The majority of the apps we use on a daily basis have a means of authenticating users. I'll show you how to easily add authentication to our Vue.js 2 application. We'll use Auth0 as our authentication service.

Auth0 allows us to issue JSON Web Tokens (JWTs). If you don't already have an Auth0 account, sign up for a free one now.

Login to your Auth0 management dashboard and let's create a new API client. If you don't already have the APIs menu item, you can enable it by going to your Account Settings and, in the Advanced tab, scroll down until you see Enable APIs Section and flip the switch.

From here, click on the APIs menu item and then the Create API button. You will need to give your API a name and an identifier. The name can be anything you choose, so make it as descriptive as you want. The identifier will be used to identify your API, this field cannot be changed once set. For our example, I'll name the API Startup Battle API and for the identifier, I'll set it as http://startupbattle.com. We'll leave the signing algorithm as RS256 and click on the Create API button.

Creating the startupbattle APICreating the Startup battle API

Next, let's define some scopes for our API. Scopes allow us to manage access to our API. We can define as few or as many scopes as we want. For our simple example, we'll just create a single scope that will grant users full access to the API.

Locate scopes barLocate Scopes bar

Adding Scope to APIAdding scope

Secure The Node API

We need to secure the API so that the private battles endpoint will only be accessible to authenticated users. We can secure it easily with Auth0.

Open up your server.js file and add the authCheck middleware to the private battles endpoint like so:

app.get('/api/battles/private', authCheck, (req,res) => {
  let privateBattles = [
    // Array of private battles
  ];

  res.json(privateBattles);
})

app.listen(3333);
console.log('Listening on localhost:3333');


Try accessing the http://localhost:3333/api/battles/private endpoint again from Postman. You should be denied access like so:

Unauthorized AccessUnauthorized Access

Next, let's add authentication to our front-end.

Adding Authentication to Our Vue.js 2 Front-End

We'll create an authentication helper to handle everything about authentication in our app. Go ahead and create an auth.js file inside the utils directory.

Before we add code, you need to install jwt-decode node package like so:

npm install jwt-decode --save


Open up the auth.js file and add code to it like so:

import decode from 'jwt-decode';
import axios from 'axios';
import Router from 'vue-router';
import Auth0Lock from 'auth0-lock';
const ID_TOKEN_KEY = 'id_token';
const ACCESS_TOKEN_KEY = 'access_token';

var router = new Router({
   mode: 'history',
});

export function login() {
  window.location.href = `https://{YOUR-AUTH0-DOMAIN}.auth0.com/authorize?scope=full_access&audience={YOUR-API-IDENTIFIER}&response_type=id_token%20token&client_id={YOUR-AUTH0-CLIENT-ID}&redirect_uri={YOUR-CALLBACK-URL}&nonce=`;
}

export function logout() {
  clearIdToken();
  clearAccessToken();
  router.go('/');
}

export function requireAuth(to, from, next) {
  if (!isLoggedIn()) {
    next({
      path: '/',
      query: { redirect: to.fullPath }
    });
  } else {
    next();
  }
}

export function getIdToken() {
  return localStorage.getItem(ID_TOKEN_KEY);
}

export function getAccessToken() {
  return localStorage.getItem(ACCESS_TOKEN_KEY);
}

function clearIdToken() {
  localStorage.removeItem(ID_TOKEN_KEY);
}

function clearAccessToken() {
  localStorage.removeItem(ACCESS_TOKEN_KEY);
}

// Helper function that will allow us to extract the access_token and id_token
function getParameterByName(name) {
  let match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash);
  return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

// Get and store access_token in local storage
export function setAccessToken() {
  let accessToken = getParameterByName('access_token');
  localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
}

// Get and store id_token in local storage
export function setIdToken() {
  let idToken = getParameterByName('id_token');
  localStorage.setItem(ID_TOKEN_KEY, idToken);
  decodeIdToken(idToken);
}

// Decode id_token to verify the nonce
function decodeIdToken(token) {
  const jwt = decode(token);
  verifyNonce(jwt.nonce);
}

// Function to generate a nonce which will be used to mitigate replay attacks
function generateNonce() {
  let existing = localStorage.getItem('nonce');
  if (existing === null) {
    let nonce = '';
    let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < 16; i++) {
        nonce += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    localStorage.setItem('nonce', nonce);
    return nonce;
  }
  return localStorage.getItem('nonce');
}

// Verify the nonce once user has authenticated. If the nonce can't be verified we'll log the user out
function verifyNonce(nonce) {
  if (nonce !== localStorage.getItem('nonce')) {
    clearIdToken();
    clearAccessToken();
  }

  window.location.href = "/";
}

export function isLoggedIn() {
  const idToken = getIdToken();
  return !!idToken && !isTokenExpired(idToken);
}

function getTokenExpirationDate(encodedToken) {
  const token = decode(encodedToken);
  if (!token.exp) { return null; }

  const date = new Date(0);
  date.setUTCSeconds(token.exp);

  return date;
}

function isTokenExpired(token) {
  const expirationDate = getTokenExpirationDate(token);
  return expirationDate < new Date();
}


In the code above, we are using a hosted version of Auth0 Lock in the loginmethod and passed in our credentials.

This URL calls the Auth0's authorize endpoint. With all the details we passed to the URL, our client app will be validated and authorized to perform authentication. You can learn more about the specific values that can be passed to the URL here.

The parameters that you do not have yet are the {YOUR-AUTH0-CLIENT-ID} and the {YOUR-CALLBACK-URL}. This will be an Auth0 client that will hold your users. When you created your API, Auth0 also created a test client which you can use. Additionally, you can use any existing Auth0 client found in Clients section of your management dashboard.

Check the Test panel of your API from the dashboard. You'll see the test client like so:

Startup ClientStartup API Client

Now, go to the clients area and check for the test client. You should see it in your list of clients like so:

Startup Battle Client

Copy the CLIENT ID and replace it with the value of YOUR-AUTH0-CLIENT-ID in the login URL. Replace your callback URL with http://localhost:8080/callback.

We also checked whether the token has expired via the getTokenExpirationDate and isTokenExpired methods. The isLoggedInmethod returns true or false based on the presence and validity of a user id_token.

We imported the Vue router and created an instance of it. We need it for redirection after login and logout.

Finally, we implemented a middleware, the requireAuth method. We'll use this method to protect the /private-battles route from being accessed for non-loggedIn users.

Let's go update the AppNav component to hide/show the login and logoutbuttons based on the user's authentication status.

Now, your AppNav component should look like this:

<template>
  <nav class="navbar navbar-default">
    <div class="navbar-header">
      <router-link to="/" class="navbar-brand"> The Ultimate Startup Battle Ground</router-link>
    </div>
    <ul class="nav navbar-nav navbar-right">
      <li>
        <button class="btn btn-danger log" v-show="isLoggedIn()" @click="handleLogout()">Log out </button>
        <button class="btn btn-info log" v-show="!isLoggedIn()" @click="handleLogin()">Log In</button>
      </li>
    </ul>
  </nav>
</template>

<script>
import { isLoggedIn, login, logout } from '../../utils/auth';

export default {
  name: 'app-nav',
  methods: {
    handleLogin() {
      login();
    },
    handleLogout() {
      logout();
    },
    isLoggedIn() {
      return isLoggedIn();
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.navbar-right { margin-right: 0px !important}

.log {
  margin: 5px 10px 0 0;
}
</style>

AppNav.vue

We imported loginlogout and isLoggedIn functions from the authhelper file. Then, we attached the login() and logout() functions to the login and logout buttons respectively.

Open up the PublicBattles Component and modify it like so:

<template>
  <div>
    <app-nav></app-nav>
    <h3 class="text-center">Daily Startup Battles</h3>
    <hr/>

    <div class="col-sm-4" v-for="battle in publicBattles">
      <div class="panel panel-default">
        <div class="panel-heading">
          <h3 class="panel-title"> {{ battle.name }} </h3>
        </div>
        <div class="panel-body">
          <p><span class="badge alert-info"> Sponsor: </span> {{ battle.sponsor }} </p>
          <p><span class="badge alert-danger"> SeedFund: </span><strong> ${{ battle.seedFund }} </strong></p>
        </div>
      </div>
    </div>
    
    <div class="col-sm-12">
      <div class="jumbotron text-center" v-if="isLoggedIn()">
        <h2>View Private Startup Battles</h2>
        <router-link class="btn btn-lg btn-success" to="/private-battles">Private Startup Battles</router-link>
      </div>
      <div class="jumbotron text-center" v-else>
        <h2>Get Access to Private Startup Battles by Logging In</h2>
      </div>
    </div>
  </div>
</template>

<script>
import AppNav from './AppNav';
import { isLoggedIn } from '../../utils/auth';
import { getPublicStartupBattles } from '../../utils/battles-api';

export default {
  name: 'publicBattles',
  components: {
    AppNav,
  },
  data() {
    return {
      publicBattles: '',
    };
  },
  methods: {
    isLoggedIn() {
      return isLoggedIn();
    },
    getPublicStartupBattles() {
      getPublicStartupBattles().then((battles) => {
        this.publicBattles = battles;
      });
    },
  },
  mounted() {
    this.getPublicStartupBattles();
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

publicBattles.vue

We are enabling the link to private startup battles based on the login status of a user via the isLoggedIn() method.

Add a Callback Component

We will create a new component and call it allback.vue. This component will be activated when the localhost:8080/callback route is called and it will process the redirect from Auth0 and ensure we received the right data back after a successful authentication. The component will store the access_token and id_token.

<template>
</template>
<script>

import { setIdToken, setAccessToken } from '../../utils/auth';

export default {
  name: '',
  mounted() {
    this.$nextTick(() => {
      setAccessToken();
      setIdToken();
    });
  },
};
</script>

callback.vue

Once a user is authenticated, Auth0 will redirect back to our application and call the /callback route. Auth0 will also append the id_token as well as the access_token to this request, and our Callback component will make sure to properly process and store those tokens in localStorage. If all is well, meaning we received an id_tokenaccess_token, and verified the nonce, we will be redirected back to the / page and will be in a logged in state.

Add Some Values to Auth0 Dashboard

Just before you try to log in or sign up, head over to your Auth0 dashboard and add http://localhost:8080/callback to the Allowed Callback URLs and http://localhost:8080 to Allowed Origins (CORS).

Secure the Private Battles Route

We need to ensure that no one can go to the browser and just type /private-battles to access the private battles route.

Open up router/index.js and modify it to import the requireAuth function and also add a beforeEnter property with a value of requireAuth to the /private-battles route like so:

import Vue from 'vue';
import Router from 'vue-router';
import PrivateBattles from '@/components/privateBattles';
import PublicBattles from '@/components/publicBattles';
import { requireAuth } from '../../utils/auth';

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: '/',
      name: 'PublicBattles',
      component: PublicBattles,
    },
    {
      path: '/private-battles',
      name: 'PrivateBattles',
      beforeEnter: requireAuth,
      component: PrivateBattles,
    },
  ],
});

index.js

One more thing. Now, let's register the /callback route in our routes file like so:

import Vue from 'vue';
import Router from 'vue-router';
import PrivateBattles from '@/components/privateBattles';
import PublicBattles from '@/components/publicBattles';
import Callback from '@/components/callback';
import { requireAuth } from '../../utils/auth';

Vue.use(Router);

export default new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'PublicBattles',
      component: PublicBattles,
    },
    {
      path: '/private-battles',
      name: 'PrivateBattles',
      beforeEnter: requireAuth,
      component: PrivateBattles,
    },
    {
      path: '/callback',
      component: Callback,
    },
  ],
});


Now, try to log in.

Lock Login WidgetLock Login Widget

For the first time, the user will be shown a user consent dialog that will show the scope available. Once a user authorizes, it goes ahead to login the user and gives them access based on the scopes.

User consent dialogUser presented with an option to authorize

Note: Since we are using localhost for our domain, once a user logs in the first time, subsequent logins will not need a user consent authorization dialog. This consent dialog will not be displayed if you are using a non-localhost domain, and the client is a first-party client.

Logged In and Unauthorized to see the Private Startup BattleLogged In, but unauthorized to see the Private Startup Battle

We have successfully logged in but the content of the private startup battle is not showing up and in the console, we are getting a 401 Unauthorized error. Why?

It's simple! We secured our endpoint earlier, but right now we are not passing the JWT to the backend yet. We need to send the JWT along with our request as a header to enable the secured endpoint's recognition of the logged-in user.

Updating the Auth and Battles API Helper

Go ahead and open up the utils/battles-api.js file. We will tweak the getPrivateStartupBattles function a bit. Currently, it initiates a GET request only to fetch data from the API.

Now, we will pass an option to send an Authorization header with a Bearer access_token along with the GET request like so:

import { getAccessToken } from './auth';

function getPrivateStartupBattles() {
  const url = `${BASE_URL}/api/battles/private`;
  return axios.get(url, { headers: { Authorization: `Bearer ${getAccessToken()}` }}).then(response => response.data);
}


The /api/battles/private endpoint will receive the token in the header and validate the user. If it is valid, the content will be provided to us.

Now, try to log in again.

Everything should work fine. Pat yourself on the back. You have just successfully built a Vue.js2 app and added authentication to it!

Conclusion

Vue.js 2 is a lightweight, fast, and awesome library for building user interfaces. Its learning curve is gentle and its API is not complex to understand. It has a fast growing community and there are many components available to the public for different functionalities.

See Part 1 and Part 2 here.

Crafter is a modern CMS platform for building modern websites and content-rich digital experiences. Download this eBook now. Brought to you in partnership with Crafter Software.

Topics:
web dev ,vue.js ,authentication ,front-end

Published at DZone with permission of Prosper Otemuyiwa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}