Bootstrap

User Authentication with Vue.js, ASP.NET Core 2 and Facebook Login

In my previous post I detailed how to setup JWT authentication with ASP.NET Core 2, Angular 5, and Facebook OAuth. It received some great feedback and also a few requests to make a Vue.js version - so here it is!

The final product is the same, simple demo we saw in the previous post with a flow to support email registration/login as well as facebook login. Both paths lead to a super-duper secure area of the app only authenticated users can access.

For brevity, in this post, I will not document how to build out the backend ASP.NET Core project used in this demo as the code and procedure are identical to the previous post. So, if you're interested in the nuts and bolts of the code and steps required to enable token authentication and authorization in ASP.NET Core go check that post out first (tip: it's the first half of the post up until the frontend starts at The Angular app section).

In this post, I will focus solely on the bits required to secure your Vue.js application using client-side token-based authentication. πŸ”

Facebook login flow
Email login flow

Development Environment

  • Windows 10
  • Sql Server Express 2017 & Sql Server Management Studio 2017
  • Visual Studio Code v1.20.1
  • Node 8.9.4 & NPM 5.6.0
  • .NET Core 2.0 sdk
  • Vue CLI => npm install -g @vue/cli https://github.com/vuejs/vue-cli
  • Vue 2.5.13

Setup

To build and run the project:

Grab the code here

Build and run the backend ASP.NET Core Web API application:

  1. Restore nuget packages with backend\AuthWebApi>dotnet restore in the backend\AuthWebApi directory.
  2. Create the database with backend\AuthWebApi>dotnet ef database update in the backend\AuthWebApi directory.
  3. Run the project with backend\AuthWebApi>dotnet run in the backend\AuthWebApi directory.

Build and run the frontend Vue.js application:

  1. Install npm packages with frontend>npm install in the frontend directory.
  2. Start the application with the node development serve frontend>npm run serve in the frontend directory.
Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Creating the app

The Vue CLI is a really handy utility for quickly getting your application created and bootstrapped with no configuration required. This means you can focus on writing the important parts of your app right out of the gate instead of spending time wiring up a compiler, linter, css preprocessor etc.. It is built on top of webpack and is easy to configure and extend with plugins.

Getting started with vue-cli is pretty straightforward.

First install globally from npm:

\>npm install -g @vue/cli

After that, use the create command and follow the prompts to set up your project.

\>vue create jwt-auth-demo

I opted to Manually select features instead of using the default template because I wanted to use TypeScript. The summary of initial selections I ended up with to compose the project looked like this:

Following that, I had to answer a few more prompts to configure my project.

  • Use class-style component syntax? -> Yes
  • Use Babel alongside TypeScript for auto-detected polyfills? -> Yes
  • CSS pre-processor -> SCSS/SASS
  • Pick a linter -> TSLint
  • Pick additional lint features -> Lint on save
  • Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? -> In dedicated config files

With all the settings plugged in, the cli tool has all the inputs required to generate our project. After a minute or two I had a new directory containing a fresh Vue.js app with all required dependencies and configuration in place.

To fire it up, change into the root of the project directory (where package.json lives) and run:

\>npm run serve

Cross your fingers, and if everything is working you should be able to hit localhost:8080 with your browser and see your new Vue.js app running.

At this point, with the "hello world" app running I changed the default port from 8080 to 8088 so it wouldn't conflict with other things running on my computer. I did this by adding a new vue.config.js and overriding the devServer settings.

devServer: {
    port: 8088
}

So, if you wish your can change the port as well to something else in vue.config.js. Note, that port number is hard-coded in a few spots within the facebook auth code so if you change it from the default of 8088 you will need to update these places with the new port for facebook login to work:

Additional npm scripts

The vue-cli-service provides us with 3 commands by default for working with the project. Out of the box, we get a zero-config development and production environment that is already structured using best practices. The dev environment comes with hot module reloading and fast building while the production configuration provides minification, module concatenation and code optimizations. If you add the unit and e2e testing options there are a few extra commands at your disposal that would make an excellent topic for a future post. πŸ˜‰

For now, the commands available to us by default are:

  • npm run serve - launches the app using the node development server at localhost:8088
  • npm run build - creates a minified, optimized build for production release (in the dist folder)
  • npm run lint - runs the linter to correct readability, maintainability, and functionality errors in our code

Add style with Bulma

Next, I wanted to spruce the app a bit so I added Bulma which is a relatively new kid on the block of modern css frameworks built on flexbox. It offers a modular sass framework to optimize your bundle as you can just import what you need but for my purposes I included everything.

I first installed Bulma:

npm install bulma

Added a new main.scss file to import Bulma and store other global styles.

@import '~bulma/bulma';

.errors-container {
background-color: $danger;
color:#fff;
padding:12px 0;
margin-top:12px
}

The last step in getting our styles wired into the app is to import main.scss into main.ts.

import Vue from 'vue';
...
// import the main Sass manifest file
import '@/assets/sass/main.scss';
...

The Registration form

With a basic app created, configured and running the way we'd like we're ready to start adding some meaningful functionality. The registration form is the first useful view in our app so let's start there.

RegistrationForm.vue contains the component definition for the registration form. The component has 2 sections: a <template> section which contains the html markup for the component and a class containing the related code which extends the Vue object. The @Component decorator identifies this class as a component in Vue's world meaning it can be outfitted with props, computed properties, watchers etc. We'll see examples of these as we explore the code further.

The class is pretty simple, it has one method handleSubmit() which calls on the account.service.ts to pass along the form data to the backend api and attempt to register the user. If the registration is successful the router navigates the user to the login form otherwise an error from the server is displayed to the user. Note, there is currently no client-side validation on the form input happening here but this would be a great exercise to extend the component.

Finally, to indicate to the user that something is happening after they submit their information we use a custom Spinner.vue component to show an animation on the form while the request is in progress.

Registration form displays a spinner and error from the server

Services

For communicating with the backend we're using the axios HTTP client library. Rather than pollute our components with a bunch of potentially repeated api calls I've abstracted these out into a few reusable services. They all inherit from BaseService.ts. The base service has some common error-handling code and the address of the ASP.NET Core api at http://localhost:5000/api.

The Login form

Upon successful registration the app navigates the user to the LoginForm.vue component. Examining this code, it is almost identical to the registration component in that we submit some credentials entered in the form to the backend and do something with the response. However, it does not call any of our services directly in handleSubmit() to authenticate the user. It dispatches an action to the store which is a centralized state container based on Vuex.

this.$store.dispatch('auth/authRequest', this.credentials)... 

Managing State with Vuex

As an application grows the challenge of managing its state and global data gets bigger as well. Data flowing through and modified by different components can be hard to keep track of and lead to buggy, hard to maintain apps and other negative consequences. React pioneered the encapsulation of state management in JavaScript applications with the flux pattern and Redux library. The Vue team created Vuex for this same purpose inside Vue.js applications. Vuex is especially suited for auth management since it’s application-scoped so it makes sense to include in this demo. The key feature of Vuex is that it acts as a centralized store for all the components in our application, with rules ensuring that the state can only be mutated in a predictable and explicit way.

Referring back to the login form, when we call on form submit:

this.$store.dispatch('auth/authRequest', this.credentials)...

We're triggering an action: authRequest in the Vuex store.

The authRequest action is defined in the auth.ts store module. It's possible to keep our entire state in a single store but is much cleaner to divide it up and keep related state/getters/actions/mutations in individual modules as our application grows.

const actions = {
    authRequest: ({commit, dispatch}: {commit: any, dispatch: any} , credentials: Credentials) => {
        return new Promise((resolve, reject) => {
            commit('authRequest');
            authService.login(credentials)
            .subscribe((result: any) => {
              localStorage.setItem('auth-token', result); // stash the auth token in localStorage
              commit('authSuccess', result);
              EventBus.$emit('logged-in', null);
              dispatch('user/userRequest', null, { root: true });
              resolve(result);
            },
            (errors: any) => {
              commit('authError', errors);
              localStorage.removeItem('auth-token');
              reject(errors);
            });
          });
}

Let's examine the authRequest action a bit closer.

  • Actions support asynchronous operations by returning Promises. This is why the body of the action is wrapped with new Promise().
  • commit('authRequest') commits a mutation by changing a state property holding the current status.
  • authService.login() calls the login method on the auth.service.ts which in turn passes the credentials to the backend api to authenticate the user. It returns an observable so we use the subscribe operator when login succeeds and process the emitted result.
  • Within subscribe a few things are happening:
    • The token returned from the backend is stored in localStorage.
    • The authSuccess mutation is committed.
    • A logged-in event is dispatched using the EventBus. There's a listener for this event in the Nav.vue component that doesn't actually do anything but log to the console but I wanted to show a method of communicating between unrelated components.
    • Finally, we dispatch a userRequest action which makes an authenticated call to the backend api to request and store the user's profile data in the user.ts store module.
  • If the backend returns a non-success response we handle the error by committing the authError mutation and removing any existing token from localStorage.

Protecting Routes with Navigation Guards

If the login form component receives a successful response from authRequest action then the router attempts to navigate the user to a protected region of the application represented by the dashboard.

This protection is provided by marking the route as requiring authentication and then hooking into the route navigation process to perform a check whenever a route requiring authentication is triggered.

Thanks to the state container we setup previously for our auth data this comes together fairly easily.

First, in router.ts we add a meta field on the dashboard route to flag it as requring authentication - I named it requiresAuth.

path: '/dashboard',
      component: DashboardRoot,
      children: [
        {
          path: 'home',
          component: DashboardHome,
          // a meta field
          meta: { requiresAuth: true },
        },
]

Next, we hook into the beforeEach guard on the global router object where we simply check the target route for the presence of the requiresAuth flag before it gets activated. If found, we call the isAuthenticated getter on the auth state store to determine whether or not we're logged in. If so, we proceed with the route navigation as normal. If not, we are redirected to the login component.

router.beforeEach((to: any, from: any, next: any) => {
  if (to.matched.some((record: any) => record.meta.requiresAuth)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!store.getters['auth/isAuthenticated']) {
      next({
        path: '/login',
        query: { redirect: to.fullPath },
      });
    } else {
      next();
    }
  } else {
    next(); // make sure to always call next()!
  }
});

The isAuthenticated getter is determined within the auth state store by simply checking for the presence of an auth token.

const getters = {
    isAuthenticated: (authState: any) => !!authState.token,
    authStatus: (authState: any) => authState.status,
    authToken: (authState: any) => authState.token,
};

If you have the demo running, try navigating to the /dashboard/home route when you're not logged in and you should be redirected back to the login view.

The Nav component

The top navigation bar in the app is provided by the Nav.vue component which contains just a little bit of markup in the template and a little bit of code to control it. We use the mapGetters helper to map a couple of store getters to a couple of local computed properties which means our component can automatically react to changes in auth state and display the appropriate links based on the current state - rad! 😎

mapGetters allows us to map local computed properties directly to store getters.

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { EventBus } from '.././event-bus';
import { mapGetters } from 'vuex';
@Component({
  computed: mapGetters({
    isAuthenticated: 'auth/isAuthenticated',
    profile: 'user/profile',
  }),
})

We can then use the computed properties to easily toggle the visibility of the links in the nav bar based on the current state.

<div class="navbar-menu">
    <div class="navbar-start">
    <a class="navbar-item" href="javascript:void(0)" v-on:click="logoff" v-show="isAuthenticated">Logoff {{profile.firstName}}</a>
    <router-link to="/register"  class="navbar-item" v-show="!isAuthenticated">Email signup</router-link>     
    <router-link to="/login"  class="navbar-item"  v-show="!isAuthenticated">Email login</router-link>
    <router-link to="/facebook-login" class="navbar-item" v-show="!isAuthenticated">Facebook signup/login</router-link>
    <router-link to="/dashboard/home"  class="navbar-item"  v-show="isAuthenticated">Dashboard</router-link>   
    </div>
</div>

The Dashboard component

As I mentioned above, the dashboard component represents the protected area of the app only authenticated users can see. In our demo, there's very little to see here. All it does is make a single authenticated request to the ASP.NET Core api to retrieve the user's profile data during the component's created lifecycle hook.

private created() {
     this.isBusy = true;
     dashboardService.getHomeDetails().then((resp: any) => {
        this.homeData = resp.data;
        this.isBusy = false;
     });
}

Creating authenticated requests

Axios makes it very easy to send authenticated requests by way of interceptors. I added one in main.ts that simply checks the auth state store on every request and if an auth token is valid it creates an Authorization header on the request attaching the token.

axios.interceptors.request.use((config: any) => {
  const authToken = store.getters['auth/authToken'];
  if (authToken) {
    config.headers.Authorization = `Bearer ${authToken}`;
  }
  return config;
}, (err: any) => {
  return Promise.reject(err);
});

Adding Facebook OAuth authentication

In the previous post, I showed an approach to integrate Facebook OAuth as a login option for the demo Angular app. Now we'll see how to do the same thing in our Vue app. Note, I'm going to avoid repeating the backend information in this post as it is identical for this demo so if you wish to learn more please refer to the previous post and repo.

Creating a Facebook application

Before we look at the code, we need a Facebook application to integrate with. For this demo, I'm reusing the Fullstack Cafe app I used in the previous post. This should work fine for you if you're running the project but if you'd like to use your own app you will need to create and configure it on Facebook's developer portal.

It's fairly quick and painless:

  1. Once you've registered, create a new app

  2. Complete the Create New App Id prompt

  3. Add a product - choose Facebook Login. For the platform choose Web

  4. This step is important, here you must add the URL that Facebook will call back to after the OAuth process completes

  5. The two key values you will need to replace in the demo project are the App Id and App Secret. These live in appsettings.json file under FacebookAuthSettings

Extending the Vue app with Facebook login

It's worth mentioning that when we look at adding 3rd-party authentication options to a website or app there are a number of existing services and frameworks out there to consider. For this demo, I'm not using any SDKs or additional Vue packages. The approach here is probably a bit crude as is and can certainly be improved upon, it is based on this guide.

To add Facebook's browser-based login flow to the existing UI I created a new FacebookLogin.vue component. The UI template is pretty simple, it's just a button that will invoke the login dialog to begin the process.

private launchFbLogin() {
    this.authWindow = window.open('https://www.facebook.com/v2.11/dialog/oauth?&response_type=token&display=popup&client_id=1528751870549294&display=popup&redirect_uri=http://localhost:8088/facebook-auth.html&scope=email', '', 'width=600,height=400');
}

This will open a new dialog window with Facebook's login page. From there, the user carries out the login process by entering their Facebook creds and connecting with the application. When that's done, Facebook will redirect back to our application where we must carry on processing the response to determine if the login succeeded and then take the appropriate action. The redirect in the demo is handled by facebook-auth.html.

There's not much happening here, just an empty page with some javascript to parse out the parameters containing the response data in the query string and then use the native window messaging api to send it back to the component via window.opener.postMessage(). The main thing we're interested in is the access_token which is received on a successful login and required by our backend Web API to carry out further validation.

// if we don't receive an access token then login failed and/or the user has not connected properly
 var accessToken = getParameterByName("access_token");
 var message = {};
 if (accessToken) {
     message.status = true;
     message.accessToken = accessToken;
  }
  else
  {
     message.status = false;
     message.error = getParameterByName("error");
     message.errorDescription = getParameterByName("error_description");
  }
  window.opener.postMessage(JSON.stringify(message), "http://localhost:8088");

Within the component's handleMessage() method we interrogate the received data to determine the authentication status. If a failure is received we show some UI to indicate there was a problem, otherwise if the authentication succeeded we dispatch a facebookAuthRequest action to the auth store.

private handleMessage(event: Event) {
  const message = event as MessageEvent;
  // Only trust messages from the below origin.
  if (message.origin !== 'http://localhost:8088') {
        return;
  }
  if (this.authWindow) {
        this.authWindow.close();
  }
  const result = JSON.parse(message.data);
  if (!result.status) {
       this.failed = true;
       this.error = result.error;
       this.errorDescription = result.errorDescription;
  } else {
       this.failed = false;
       this.isBusy = true;
       this.$store.dispatch('auth/facebookAuthRequest', result.accessToken).then((fbResult) => {
           this.$router.push('/dashboard/home');
       })
       .catch((err) => {
          this.errors = err;
          this.failed = true;
        })
        .then(() => {
          this.isBusy = false;
        });
     }
}

The facebookAuthRequest action calls facebookLogin() on the auth service passing along the access token we received from facebook. This will get validated on the backend. From there, the flow is the same as the credential-based login we looked at earlier. We simply process a successful response accordingly or handle any errors returned from the ASP.NET Core Web API.

facebookAuthRequest: ({commit, dispatch}: {commit: any, dispatch: any} , accessToken: string) => {
        return new Promise((resolve, reject) => {
          commit('authRequest');
          authService.facebookLogin(accessToken)
          .subscribe((result: any) => {
            localStorage.setItem('auth-token', result); // stash the auth token in localStorage
            commit('authSuccess', result);
            EventBus.$emit('logged-in', null);
            dispatch('user/userRequest', null, { root: true });
            resolve(result);
          },
          (errors: any) => {
            commit('authError', errors);
            localStorage.removeItem('auth-token');
            reject(errors);
          });
        });
}

Wrapping up

Vue has seen phenomenal growth over the past year (currently 85K+ stars on GitHub). I believe this is largely due to the fact that it is easy to learn, has pretty good documentation, is lightweight and fast. These traits give a developer the sense that Vue really aims to get out of your way which naturally leads to a feeling of greater productivity. I hope you find this post helpful and if you have any questions or feedback please let me know in the comments below. Finally, security in any application demands careful consideration, design and testing so please use this as a guide and be sure to adequately assess and thoroughly test any security implementation in real-world projects!

source code here

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.