Vue.js Authentication System with Node.js Backend

Jscrambler - Feb 8 '19 - - Dev Community

This article was originally published on the Jscrambler Blog by Lamin Sanneh.

When building a Vue.js authentication system, there are two primary scenarios involved. Scenario one: one party controls both the front-end and back-end; scenario two: a third-party controls the back-end. If it is the latter case, we have to adapt the front-end. It has to deal with whatever authentication type becomes available from the back-end.

The finished code for this tutorial is available at these GitHub repositories:

Front-end JavaScript Authentication Types

In our application, we have the flexibility to choose between various authentication types. This is because we will be in charge of both front-end and back-end. Let us identify the two types. The first one is local or same-domain authentication — this is the case when the front-end and the back-end are both running on the same domain. The second is cross-domain authentication — it is when the front-end and back-end are running on different domains.

These are the two main categories but there are many sub-categories under them. In light of the above, we will use local authentication since we are in charge of the whole system. We will be using many Node.js libraries. But the two main ones are Passport.js and Express.js. Passport.js is an authentication library. It provides several features like local authentication, OAuth authentication and Single Sign-On authentication. Express.js is a server framework for Node.js used for building web applications.

The Application Paradigm

Our application front-end will have two main pages: a login page and a dashboard page. Both authenticated and anonymous users will have access to the login page. The dashboard page will only be accessible to authenticated users. The login page will have a form which will submit data through Ajax to our back-end API. Then, the back-end will check if the credentials are correct and reply back to the front-end with a cookie. This cookie is what the front-end will use to gain access to any locked pages.

Revalidation of the cookie happens on every request to a locked page. If the cookie becomes invalid or the user is not logged in, they cannot access the dashboard. The back-end will send an error response and the front-end will know to redirect the user back to the login page.

We will not be setting up a real database — we will use an array of users in the back-end to mimic some form of a database. Finally, we will have a logout link. This will send a request to our server to invalidate our current session and hence log out the current user.

So, let’s begin building our Vue.js authentication system using Node.js as a back-end.

Vue.js Front-End Setup

To begin with, we first need to have the latest version of Node.js and vue-cli setup. At the time of this article, the latest version of vue-cli is version 3. If the installed version is 2, we want to upgrade — we first need to remove the old version by running:

npm uninstall vue-cli -g
Enter fullscreen mode Exit fullscreen mode

Then install the latest version by running:

npm install -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

followed by

npm install -g @vue/cli-init
Enter fullscreen mode Exit fullscreen mode

After setting up the above, go to any folder in the terminal and run:

vue init webpack vueauthclient
Enter fullscreen mode Exit fullscreen mode

This will create a new application in vueauthclient using the webpack folder organization.

We should get some prompts on the command line. It is safe to select all the defaults — but for this article, we can select “no” for the tests. Next, navigate to this folder using cd vueauthclient and run the application using:

npm run dev
Enter fullscreen mode Exit fullscreen mode

This will launch a development server which is accessible at the URL localhost:8080. After visiting this URL, the Vue.js logo with some text should be visible on the page. The Vue.js component responsible for displaying this page lives in the file:

vueauthclient/src/components/HelloWorld.vue
Enter fullscreen mode Exit fullscreen mode

Main Login Screen

Let us set up our login page. Then, we will change the homepage to default to the login page screen which we are yet to create. From now on, we will leave out the main application folder vueauthclient, when referring to files.

Let us install the Ajax library called Axios using:

npm install axios --save
Enter fullscreen mode Exit fullscreen mode

This is a library which makes it easier to do HTTP Ajax calls to any back-end server. It is available for both front-end and back-end applications but here, we will only use it on the front-end.

Next, create a login component file in src/components/Login.vue. In this file, paste the following:

<template>
    <div>    
        <h2>Login</h2>    
        <form v-on:submit="login">    
            <input type="text" name="email" /><br>    
            <input type="password" name="password" /><br>    
            <input type="submit" value="Login" />    
        </form>    
    </div>
</template>

<script>
    import router from "../router"    
    import axios from "axios"    
    export default {    
        name: "Login",    
        methods: {    
            login: (e) => {    
                e.preventDefault()    
                let email = "user@email.com"   
                let password = "password"    
                let login = () => {    
                    let data = {    
                        email: email,    
                        password: password    
                    }    
                    axios.post("/api/login", data)    
                        .then((response) => {    
                            console.log("Logged in")    
                            router.push("/dashboard")    
                        })    
                        .catch((errors) => {    
                            console.log("Cannot log in")    
                        })    
                }    
                login()    
            }    
        }    
    }
</script>
Enter fullscreen mode Exit fullscreen mode

Let's break down this code to see what is happening.

The template part below is a form with two input fields: email and password. The form has a submit event handler attached to it. Using the Vue.js syntax v-on:submit="login", this will submit the field data to the login component method.

<template>
    <div>
        <h2>Login</h2>
        <form v-on:submit="login">
            <input type="text" name="email" /><br>
            <input type="password" name="password" /><br>    
            <input type="submit" value="Login" />    
        </form>    
    </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In the script part of the code, as shown below, we are importing our router file. This lives in src/router/index.js. We are also importing the Axios ajax library for the front-end. Then, we are storing the user credentials and making a login request to our back-end server:

<script>
    import router from "../router"        
    import axios from "axios"    
    export default {    
        name: "Login",    
        methods: {    
            login: (e) => {    
                e.preventDefault()   
                let email = "user@email.com"
                let password = "password"
                let login = () => {
                    let data = {
                        email: email,
                        password: password
                    }
                    axios.post("/api/login", data)
                        .then((response) => {
                            console.log("Logged in")
                            router.push("/dashboard")
                        })
                        .catch((errors) => {
                            console.log("Cannot login")
                        })
                }
                login()
            }
        }
    }
</script>
Enter fullscreen mode Exit fullscreen mode

In the script area below,

e.preventDefault()
let email = "[user@email.com](mailto:user@email.com)"
let password = "password"
Enter fullscreen mode Exit fullscreen mode

We are storing hard-coded username and password in variables for now. This helps speed up development by preventing us from retyping the same thing. Later, we will switch those out and get the real data from the form submission.

In the final part of the code below, we are making an ajax call using the credentials above. In the case of an ok response from the server, we redirect the user to the dashboard. If the response is not ok, we stay on the same page and log an error in the console.

let login = () => {
  let data = {
    email: email,
    password: password
  }
  axios.post("/api/login", data)
    .then(response => {
      console.log("Logged in")
      router.push("/dashboard")
    })
    .catch(errors => {
      console.log("Cannot login")
    })
}
login()
Enter fullscreen mode Exit fullscreen mode

Now that we have our login component set up, let’s change the router to make sure it recognizes the new page. In the file src/router/index.js, change the existing router to this:

import Vue from "vue"
import Router from "vue-router"
import Login from "@/components/Login"
import HelloWorld from "@/components/HelloWorld"
Vue.use(Router)
export default new Router({
  routes: [
    {
      path: "/",
      name: "HelloWorld",
      component: HelloWorld
    },
    {
      path: "/login",
      name: "Login",
      component: Login
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

What we’ve done is import our new component, then add an object to the routes array. Remove the HelloWorld route registration, as we won't be needing it anymore.

Finally, for the login, page, let's make sure it is the default page of our application. Change the current path of the login route registration from

path: "/login",
Enter fullscreen mode Exit fullscreen mode

to

path: "/",
Enter fullscreen mode Exit fullscreen mode

Don’t forget to delete the route registration for the HelloWorld route or else an error might occur. Navigating to localhost:8080 again in the browser, we should see our new login form. Submitting it at this stage will not do anything except complain that the back-end URL localhost:8080/api/login does not exist.

Setup First Secure Page - The Dashboard

Now onto the dashboard page. Create a component for it by making a file at src/components/Dashboard.vue. In there, paste the following:

<template>
    <div>    
        <h2>Dashboard</h2>    
        <p>Name: {{ user.name }}</p>    
    </div>
</template>
<script>
    import axios from "axios"    
    import router from "../router"    
    export default {    
        name: "Login",    
        data() {    
            return {    
                user: {    
                    name: Jesse    
                }    
            }    
        },    
        methods: {    
            getUserData: function() {    
                let self = this    
                axios.get("/api/user")    
                    .then((response) => {    
                        console.log(response)    
                        self.$set(this, "user", response.data.user)    
                    })    
                    .catch((errors) => {    
                        console.log(errors)    
                        router.push("/")    
                    })    
            }    
        },    
        mounted() {    
            this.getUserData()    
        }    
    }
</script>
Enter fullscreen mode Exit fullscreen mode

In the template section, we are displaying the current username. Before setting up the back-end, we will hardcode a user in the front-end. This is so that we can work with this page or else we will get an error.

In the script section, we are importing Axios library and our router. Then, we have a data function for our component where we return an object with a user property. As we can see, we currently have some hardcoded user data.

We also have two methods called getUserData and mounted. The Vue.js engine calls the mounted method when the component has opened. We only have to declare it. The second method, getUserData is called in the mounted method. In there, we are making a call to the back-end server to fetch the data for the currently logged in user.

During the call to the back-end, we get a response from the server. We will have to handle two possible scenarios depending on the response type.
First, if the call was successful, we set the user property with the data returned from the back-end using:

self.$set(this, "user", response.data.user)
Enter fullscreen mode Exit fullscreen mode

Secondly, if there was a login issue, the server responds with an error. Then, the front-end redirects the user back to the login page with this line:

router.push("/")
Enter fullscreen mode Exit fullscreen mode

We use the push method above for redirection and it is available in the package called vue-router, the default router for Vue.js. Let’s add in the route config for this page by adding this to the route file, like we did for the login page. Import the component:

import Dashboard from "@/components/Dashboard"
Enter fullscreen mode Exit fullscreen mode

And add the route definition:

{
    path: "/dashboard",
    name: "Dashboard",
    component: Dashboard
}
Enter fullscreen mode Exit fullscreen mode

Setup Front-End Data Layer with Axios

Now that we have our front-end pages in place, let’s configure Axios and Vue.js. We will make them ready to communicate with our back-end. Because we are in the development phase, the front-end is running on port 8080. Once we start developing our back-end server, it will be running on a different port number 3000. This will be the case until we are ready for production.

There is nothing stopping us from running them on the same port. In fact, will eventually be the case in the end. If we recollect, we are going for the same-domain approach. We will run the back-end and front-end on different ports for now. This is because we want to take advantage of the many useful features of the Vue.js development server. We will touch on how to merge the two (front and back-end) in a later chapter.

Before moving on, let's highlight one issue here. There is a drawback to developing our application on different ports. It is called Cross-Origin Request Sharing, shortly named CORS. By default, it will not allow us to make cross-domain Ajax requests to our back-end. There is a Node.js library to find a way around that but we will leave that for another tutorial.

Protect your Vue App with Jscrambler

The Vue.js development server has something called proxying. It allows our back-end server to think that the front-end is running on the same port as itself. To enable that feature, open up the config file in config/index.js. Under the dev property, add in an object like so:

proxyTable: {

"/api": "http://localhost:3000"

},
Enter fullscreen mode Exit fullscreen mode

In the above code, we are rerouting Ajax requests that begin with /api to the URL http://localhost:3000. Notice that this is different from the URL our front-end application is running on. If we did not have this code, the Ajax requests by default are sent to http://localhost:8080, which is not what we want. When ready for production, we can remove it:

Finally, install the front-end cookie library using:

npm install vue-cookies --save
Enter fullscreen mode Exit fullscreen mode

Securing our Back-End API

Let’s now move onto setting up a Node.js back-end. First of all, we need to have Node.js installed on your system for this part as well. Head over to a terminal window. Create an empty folder called vueauthclient-backend. Navigate to the folder using:

cd vueauthclient-backend
Enter fullscreen mode Exit fullscreen mode

Then initialize a new Node.js application using the command:

npm init
Enter fullscreen mode Exit fullscreen mode

There will be several prompts. Let's accept the defaults and specify values where required. We should end up with a file called package.json. Create a file called index.js in the project's root directory. This is where our main code will live. Install several libraries using the command:

npm install --save body-parser cookie-session express passport passport-local
Enter fullscreen mode Exit fullscreen mode
  • the body-parser library allows us to access values from an Ajax request sent from a front-end.
  • cookie-session allows us to store cookies on the server and to be able to send one back to a client when they log in.
  • express is our Node.js framework which helps us build Ajax APIs. It also allows us to serve static files from our Node.js application.
  • passport.js is a library to help us authenticate users. It does this by creating sessions and managing them for each user.
  • passport-local is a library component for Passport.js. It specializes in simple authentication by using the local authentication type. For example, if we want to use SSO login type, we will need to install the component of Passport.js that has that feature. So now that we have our libraries installed, let’s import and set them up.

At the top of the index.js file, import the libraries using the code:

const express = require('express')

// creating an express instance
const app = express()
const cookieSession = require('cookie-session')
const bodyParser = require('body-parser')
const passport = require('passport')

// getting the local authentication type
const LocalStrategy = require('passport-local').Strategy
Enter fullscreen mode Exit fullscreen mode

First, let’s initialize the cookie-session and the body-parser libraries using:

app.use(bodyParser.json())

app.use(cookieSession({
    name: 'mysession',
    keys: ['vueauthrandomkey'],
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
}))
Enter fullscreen mode Exit fullscreen mode

We are setting the cookie to expire after 24 hours. Next, let’s instruct our Node.js app that we want to use Passport.js. Do that by adding the line:

app.use(passport.initialize());
Enter fullscreen mode Exit fullscreen mode

Next, tell Passport.js to start its session management system:

app.use(passport.session());
Enter fullscreen mode Exit fullscreen mode

Since we won’t be using a real database for managing users, for brevity’s sake we will use an array for that. Add in the following lines:

let users = [
  {
    id: 1,
    name: "Jude",
    email: "user@email.com",
    password: "password"
  },
  {
    id: 2,
    name: "Emma",
    email: "emma@email.com",
    password: "password2"
  }
]
Enter fullscreen mode Exit fullscreen mode

Next, let’s set up the URLs for logging in, logging out and getting user data. These will be found at POST /api/login, GET /api/logout and GET /api/user, respectively. For the login part, paste in the following:

app.post("/api/login", (req, res, next) => {
  passport.authenticate("local", (err, user, info) => {
    if (err) {
      return next(err);
    }

    if (!user) {
      return res.status(400).send([user, "Cannot log in", info]);
    }

    req.login(user, err => {
      res.send("Logged in");
    });
  })(req, res, next);
});
Enter fullscreen mode Exit fullscreen mode

In here, we are instructing Express.js to authenticate the user using the supplied credentials. If an error occurs or if it fails, we return an error message to the front-end. If the user gets logged in, we will respond with a success message. Passport.js handles the checking of credentials. We will set that up shortly. Note that the method passport.authenticate resides in the Passport.js library.

The next URL we will set up is logout. This invalidates our cookie if one exists. Add this to achieve the functionality:

app.get("/api/logout", function(req, res) {
  req.logout();

  console.log("logged out")

  return res.send();
});
Enter fullscreen mode Exit fullscreen mode

Finally, the URL to get the currently logged in users’ data. When logged in, Passport.js adds a user object to the request using the cookie from the front-end as an identifier. We have to use the id from that object to get the required user data from our array of data in the back-end. Paste in the following:

app.get("/api/user", authMiddleware, (req, res) => {
  let user = users.find(user => {
    return user.id === req.session.passport.user
  })

  console.log([user, req.session])

  res.send({ user: user })
})
Enter fullscreen mode Exit fullscreen mode

Notice that, this time, we have a second variable we are passing in before the callback. This is because we want to protect this URL, so we are passing a middleware filter. This filter will check if the current session is valid before allowing the user to proceed with the rest of the operation. Let’s create the middleware using:

const authMiddleware = (req, res, next) => {
  if (!req.isAuthenticated()) {
    res.status(401).send('You are not authenticated')
  } else {
    return next()
  }
}
Enter fullscreen mode Exit fullscreen mode

We have to make sure to declare it before creating the API route for /api/user.

Next, let’s configure Passport.js so it knows how to log us in. After logging in, it will store the user object data in a cookie-session, and retrieve the data on later requests. To configure Passport.js using the local strategy, add in the following:

passport.use(
  new LocalStrategy(
    {
      usernameField: "email",
      passwordField: "password"
    },

    (username, password, done) => {
      let user = users.find((user) => {
        return user.email === username && user.password === password
      })

      if (user) {
        done(null, user)
      } else {
        done(null, false, { message: 'Incorrect username or password'})
      }
    }
  )
)
Enter fullscreen mode Exit fullscreen mode

In here, we are instructing Passport.js to use the LocalStrategy we created above. We are also specifying which fields to expect from the front-end as it needs a username and password. Then, we are using those values to query the user. If these are valid, we call the done callback, which will store the user object in the session. If it is not valid, we will call the done callback with a false value and return with an error. One thing to note is that the above code works in conjunction with the login URL. The call to passport.authenticate in that URL callback triggers the above code.

Next, let’s tell Passport.js how to handle a given user object. This is necessary if we want to do some work before storing it in session. In this case, we only want to store the id as it is enough to identify the user when we extract it from the cookie. Add in the following to achieve that:

passport.serializeUser((user, done) => {
  done(null, user.id)
})
Enter fullscreen mode Exit fullscreen mode

Next, let’s set up the reverse. When a user makes a request for a secured URL. We tell passport how to retrieve the user object from our array of users. It will use the id we stored using the serializeUser method to achieve this. Add this:

passport.deserializeUser((id, done) => {
  let user = users.find((user) => {
    return user.id === id
  })

  done(null, user)
})
Enter fullscreen mode Exit fullscreen mode

Now, let’s add the code which boots up the Node.js server using the following:

app.listen(3000, () => {
  console.log("Example app listening on port 3000")
})
Enter fullscreen mode Exit fullscreen mode

Run the command:

node index.js
Enter fullscreen mode Exit fullscreen mode

This actually starts the server. There will be a message in the console with the text Example app listening on port 3000.

Getting Ready for Production

Now, when we visit the page localhost:8080, we should see a login form. When we submit the form, we get redirected to the dashboard page. We achieve this using the proxy we set up earlier.

This is acceptable for development — but it defeats the purpose of having a same-domain application. To have a same-domain scenario, we need to compile our application for production.

Before that, let's test that the proxy is working. Comment out the proxy URL code in config/index.js. We may need to restart the development server because we changed a config file.

Now, let's revisit the login page and submit the form. We will get an error saying that we aren’t allowed access to the back-end server. To get around this, we need to configure our Node.js back-end server. The back-end will now serve our front-end application instead of the development server.

In the console for the front-end, run the command:

npm run build
Enter fullscreen mode Exit fullscreen mode

This will generate all the necessary files needed for production. We can find all the created files from this command in the dist folder. From this point onwards, we have two options: we can either copy this folder over so it is part of our Node.js application or we can tell the Node.js server to refer directly to it on our file system. The latter is useful if we still want them as separate repositories. We will use the latter method.

Navigate to the folder dist. Run the command pwd to get the absolute path of the dist folder, assuming we are on a Linux based system or Mac. If we are on Windows, we can get the absolute path to the folder using an equivalent command.

Copy the absolute path but do not forget to restart the Node.js server after any modification. Since we do not want to keep restarting the server, let's install nodemon. It can handle that for us when our code changes.

Next, paste in the following after the import statements:

const publicRoot = '/absolute/path/to/dist'

app.use(express.static(publicRoot))
Enter fullscreen mode Exit fullscreen mode

This is telling the server where to look for files.

The final step will be to add a route to the root of our Node.js application. This is so it serves the production-ready code we had compiled. Do that by adding:

app.get("/", (req, res, next) => {
  res.sendFile("index.html", { root: publicRoot })
})
Enter fullscreen mode Exit fullscreen mode

Now, even with the proxy disabled, let's visit the server root localhost:3000. We will see the login form. Submit this and we should see the dashboard page with the username displayed.

Logout Functionality and Login Data

Note that our application is still using hardcoded data, we want to get that from the submitted form. Change these lines in the login component from:

let email = "user@email.com"

let password = "password"
Enter fullscreen mode Exit fullscreen mode

to:

let email = e.target.elements.email.value

let password = e.target.elements.password.value
Enter fullscreen mode Exit fullscreen mode

Now, we are using the data from the form. Next, let’s set up a link to log us out. In the component src/App.vue, change the template to this:

<template>
    <div id="app">    
        <img src="./assets/logo.png">    
        <div>    
            <router-link :to="{ name: 'Dashboard'}">Dashboard</router-link>    
            <router-link :to="{ name: 'Login'}">Login</router-link>    
            <a href="#" v-on:click="logout">Logout</a>    
        </div>    
        <router-view/>    
    </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Here, we have created links to the login page, the dashboard page, and a logout link. The logout link does not have a corresponding method currently, so let’s create that. In src/App.vue add a logout method in the scripts section:


logout: function (e) {
    axios
      .get("/api/logout")
      .then(() => {
        router.push("/")
      })
}
Enter fullscreen mode Exit fullscreen mode

Here, we are making an Ajax request to the Node.js back-end. Then, we redirect the user to the login page when the response has returned. The logout will not work with our deployed app because we need to redeploy it again for production using:

npm run build
Enter fullscreen mode Exit fullscreen mode

Now, we can revisit the URL localhost:3000. We can log in, log out and visit the dashboard page.

Conclusion

After this tutorial, we should be able to add as many authenticated pages as we want.

If this is the first time using Vue.js, please refer to our introductory blog post here. It will help in setting up and building a basic Vue.js application.

Also, don’t forget to protect you Vue.js application from code theft and reverse engineering. See our handy guide on protecting Vue.js apps with Jscrambler.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .