How to Build Production-ready Vue Authentication

Nader Dabit - Mar 4 '19 - - Dev Community

Production-ready Vue Authentication with AWS

To view the final code for this project at any time, click here.

In this tutorial, you'll learn how to build a real authentication flow into your Vue application using Vue Router, AWS Amplify, & Amazon Cognito. While the identity provider we'll be using is AWS with Amazon Cognito, the fundamental design of our application will be provider agnostic meaning you should be able to follow along using the provider of your choice.

Authentication overview

If you've ever tried rolling your own authentication service & implementation (both on the front & back end), you're already aware of the pain that comes along with it.

Thankfully today we have many amazing identity services & providers that handle all of this for us. You may already be familiar with services like Auth0, Okta, & Amazon Cognito that do the hard work behind the scenes so you don't have to by implementing the user & identity management that is a necessary requirement for most modern applications.

In this tutorial you'll learn how you can manage everything from user sign up, user sign in, forgot password, & MFA. You'll also learn how to implement protected client side routing with Vue Router so you can define which routes can be public & which routes need to be protected for only logged-in users.

By the end of this tutorial you'll have a good grasp on building & deploying Vue applications with enterprise-level security & authentication enabled.

Getting Started

Creating the Vue project

The first thing we'll do is scaffold a new Vue application using the Vue CLI. If you do not already have the Vue CLI installed, click here to follow the installation instructions.

~ vue create vue-auth

? Please pick a preset: default

cd vue-auth
Enter fullscreen mode Exit fullscreen mode

Once the project has been created & you are inside of the directory, let's install the necessary dependencies we'll need using either npm or yarn:

~ yarn add vue-router aws-amplify @aws-amplify/ui-vue
Enter fullscreen mode Exit fullscreen mode

What is aws-amplify-vue? AWS Amplify has platform specific components that allow us to quickly scaffold & get up and running with important functionality like authentication flows, image uploads, & more.

Creating the folder structure

Let's now create the files that we'll be using to implement the authentication flow. Inside the src directory, create the following files:

~ touch router.js components/Auth.vue components/Home.vue components/Profile.vue components/Protected.vue
Enter fullscreen mode Exit fullscreen mode

Working with Amplify

Installing the Amplify CLI

To add the authentication service we'll be using the AWS Amplify CLI. Let's go ahead and install that now:

~ npm install -g @aws-amplify/cli
Enter fullscreen mode Exit fullscreen mode

Next, we'll need to configure the CLI. To do so, run the following command:

~ amplify configure
Enter fullscreen mode Exit fullscreen mode

For a full walkthrough of how to configure the CLI, check out this video.

Now that we have our project created & the CLI installed we can create the authentication service we'll be using. To do so, we'll initialize a new Amplify project & then add authentication to it.

Initializing the Amplify project

To initialize a new Amplify project, run the init command:

~ amplify init
Enter fullscreen mode Exit fullscreen mode

The init will initialize the project & walk you through some steps to configure your project name, environment, & other build settings. Choose a project & environment name, & then choose the default for the rest of the questions.

Adding the authentication service

Now that the Amplify project has been initialized, we can add the authentication service:

~ amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No

~ amplify push
Enter fullscreen mode Exit fullscreen mode

The default security configuration will give us smart defaults for this project. If you would like a custom configuration you can choose No, or you can later run amplify update auth.

After amplify push finished running successfully, the authentication has been successfully created & we can now start writing our code!

You should notice you now have a file called aws-exports.js (holds base project configuration) in your src directory & a folder called amplify (hold detailed project configuration & custom code) in your root directory.

Writing the code

We'll be implementing authentication in two ways:

  1. Part 1 - Using the preconfigured amplify-authenticator component from AWS Amplify Vue to quickly get our auth flow up & running.
  2. Part 2 - Building out an entirely custom authentication flow.

Part 1 - Using the preconfigured amplify-authenticator component

Next we'll need to update main.js to configure the Vue project to work with Amplify & our new aws-exports.js file. We'll also need to let our application know about the router that we will be creating in the next step.

src/main.js

// src/main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Amplify from 'aws-amplify'
import '@aws-amplify/ui-vue'
import config from './aws-exports';

import App from './App'
import router from './router'

Amplify.configure(config)
Vue.use(VueRouter)
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  router
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

Next, we'll configure our router. This is where we will also place the custom logic for the protected routes.

src/router.js

// src/router.js
import VueRouter from 'vue-router'
import { Auth } from 'aws-amplify'

import Home from './components/Home'
import Profile from './components/Profile'
import AuthComponent from './components/Auth'
import Protected from './components/Protected'

const routes = [
  { path: '/', component: Home },
  { path: '/auth', component: AuthComponent },
  { path: '/protected', component: Protected, meta: { requiresAuth: true} },
  { path: '/profile', component: Profile, meta: { requiresAuth: true} }
]

const router = new VueRouter({
  routes
})

router.beforeResolve((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    Auth.currentAuthenticatedUser().then(() => {
      next()
    }).catch(() => {
      next({
        path: '/auth'
      });
    });
  }
  next()
})

export default router
Enter fullscreen mode Exit fullscreen mode
Details of src/router.js
  1. We import Vue & VueRouter
  2. We import the components we'll be using in our routes
  3. We define an array of routes. We add an additional meta property to specify the routes requiring authentication by using a Boolean named requiresAuth.
  4. We create the router variable
  5. We use the beforeResolve guard from Vue Router, which will be called right before the navigation is confirmed, to check if the user is authenticated. If they are authenticated, we allow them onto the next route. If they are not, we redirect them to the sign up page (/auth).

Next, let's create the authentication component.

src/components/Auth.vue

// src/components/Auth.vue
<template>
  <div class="auth">
    <amplify-authenticator></amplify-authenticator>
  </div>
</template>

<script>

export default {
  name: 'auth'
}
</script>

<style>
.auth {
  margin: 0 auto;
  width: 460px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/Auth.vue

This is a pretty basic component that does quite a bit under the hood! The amplify-authenticator Vue component will actually scaffold out the entire authentication flow for us (Sign-up, Sign-in, & forgot password).

Now we'll update the App component. This component will be doing a few things:

  1. Displaying the navigation links
  2. Rendering the router
  3. Holding most authentication logic for listening to user sign-in / sign-out.

src/App.vue

// src/App.vue
<template>
  <div id='app'>
    <div  class='nav'>
      <router-link tag="p" to="/">
        <a>Home</a>
      </router-link>
      <router-link tag="p" to="/profile">
        <a>Profile</a>
      </router-link>
      <router-link tag="p" to="/protected">
        <a>Protected</a>
      </router-link>
      <router-link tag="p" to="/auth" v-if="!signedIn">
        <a>Sign Up / Sign In</a>
      </router-link>
    </div>
    <router-view></router-view>
    <div class='sign-out'>
      <amplify-sign-out v-if="signedIn"></amplify-sign-out>
    </div>
  </div>
</template>

<script>
import { Auth, Hub } from 'aws-amplify'

export default {
  name: 'app',
  data() {
    return {
      signedIn: false
    }
  },
  beforeCreate() {
    Hub.listen('auth', data => {
      console.log('data:', data)
      const { payload } = data
      if (payload.event === 'signIn') {
        this.signedIn = true
        this.$router.push('/profile')
      }
      if (payload.event === 'signOut') {
        this.$router.push('/auth')
        this.signedIn = false
      }
    })
    Auth.currentAuthenticatedUser()
      .then(() => {
        this.signedIn = true
      })
      .catch(() => this.signedIn = false)
  }
}
</script>

<style>
.nav {
  display: flex;
}
.nav p {
  padding: 0px 30px 0px 0px;
  font-size: 18px;
  color: #000;
}
.nav p:hover {
  opacity: .7;
}
.nav p a {
  text-decoration: none;
}
.sign-out {
  width: 160px;
  margin: 0 auto;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/App.vue

  1. We use the amplify-sign-out component to render a sign out button if the user is signed in.
  2. We create a Boolean called signedIn & set it to false when the app loads
  3. In the beforeCreate lifecycle method we listen for the authState event by using the Hub API. If we detect a sign in, we redirect them to view their profile & set signedIn to true. If we detect a sign out, we redirect them to the /auth route & set signedIn to false.
  4. When the app loads, we also call Auth.currentAuthenticatedUser to check whether or not the user is signed in & set the signedIn variable appropriately.

Next, let's add the Profile component.

This basic component will display the user's username that we'll be retrieving using Amplify.

src/components/Profile.vue

// src/components/Profile.vue
<template>
  <h1>Welcome, {{user.username}}</h1>
</template>

<script>
import { Auth } from 'aws-amplify'

export default {
  name: 'Profile',
  data() {
    return {
      user: {}
    }
  },
  beforeCreate() {
    Auth.currentAuthenticatedUser()
      .then(user => {
        this.user = user
      })
      .catch(() => console.log('not signed in...'))
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/Profile.vue

The main thing to note about this component is that we're retrieving information about the user by calling the Auth.currentAuthenticatedUser method. This method will return a user object containing metadata about the logged in user or it will error out if the user is not signed in.

Now we can create the last two basic components.

src/components/Home.vue

// src/components/Home.vue
<template>
  <h1>Home</h1>
</template>

<script>
export default {
  name: 'home',
}
</script>
Enter fullscreen mode Exit fullscreen mode

src/components/Protected.vue

// src/components/Protected.vue
<template>
  <h1>Hello from protected route!</h1>
</template>

<script>

export default {
  name: 'protected',
}
</script>
Enter fullscreen mode Exit fullscreen mode

Testing it out

Part 1 of our application is finished, so let's test it out:

~ npm run serve
Enter fullscreen mode Exit fullscreen mode

When the app loads we should be able to only view the Home route. If we try navigate to one of the protected routes, we should be redirected to the authentication screen.

Once we're signed in we should be able to view the protected pages.

You'll notice that the user is persisted. This is handled for you by the Amplify client library. In order to sign out, you must explicitly click the sign out button we've rendered or use the Auth.signOut method from the Auth category.

Now that we've gotten this up & running, what's next? Well, the amplify-authenticator component can be customized to a certain extent to control fields rendered as well as the styling (to learn how, check out the docs here) but what if we'd like to have an entirely customized authentication flow? Let's do this now.

Part 2 - Building out a custom authentication flow.

Now that we've gotten authentication working, let's update what we have to be able to be customized. Right now all of our auth functionality is stored in Auth.vue. In this file we're using the amplify-authenticator component to scaffold out our entire authentication flow. Let's update our app to have custom authentication.

The first thing we'll need to do is create a couple of new files in our components directory, one for signing users in & one for signing up new users.

touch src/components/SignIn.vue src/components/SignUp.vue
Enter fullscreen mode Exit fullscreen mode

Next, let's update Auth.vue to use the new files & add some new functionality. In this file we'll render the SignUp & SignIn components depending on some component state. We'll also render a link that allows us to toggle between sign up & sign in state:

src/components/Auth.vue

// src/components/Auth.vue
<template>
  <div class="auth">
    <sign-up :toggle='toggle' v-if="formState === 'signUp'"></sign-up>
    <sign-in v-if="formState === 'signIn'"></sign-in>
    <p v-on:click="toggle" class="toggle">{{ formState === 'signUp' ?
      'Already sign up? Sign In' : 'Need an account? Sign Up'
      }}</p>
  </div>
</template>

<script>
import SignUp from './SignUp'
import SignIn from './SignIn'

export default {
  name: 'app',
  data() {
    return {
      formState: 'signUp'
    }
  },
  methods: {
    toggle() {
      this.formState === 'signUp' ? this.formState = 'signIn' : this.formState = 'signUp'
    }
  },
  components: {
    SignUp,
    SignIn
  }
}
</script>

<style>
.auth {
  margin: 0 auto;
  width: 460px;
}
.toggle {
  cursor: pointer;
  font-size: 18px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/Auth.vue

The main thing to take into consideration here is that we are importing our two new components & rendering either of them based on the value of the formState Boolean. Nothing really too interesting yet.

Next, let's create the sign up form.

src/components/SignUp.vue

// src/components/SignUp.vue
<template>
  <div>
    <h2>{{ formState === 'signUp' ? 'Sign Up' : 'Confirm Sign Up' }}</h2>
    <div class='formcontainer' v-if="formState === 'signUp'">
      <input placeholder="username" v-model='form.username' class='input' />
      <input placeholder="password" type='password' v-model='form.password' class='input' />
      <input placeholder="email" v-model='form.email' class='input' />
      <button v-on:click='signUp' class='button'>Sign Up</button>
    </div>
    <div class='formcontainer' v-if="formState === 'confirmSignUp'">
      <input placeholder="confirmation code" v-model='form.authCode' class='input' />
      <button v-on:click='confirmSignUp' class='button'>Confirm Sign Up</button>
    </div>
  </div>
</template>

<script>
import { Auth } from 'aws-amplify'

export default {
  name: 'home',
  props: ['toggle'],
  data() {
    return {
      formState: 'signUp',
      form: {
        username: '',
        password: '',
        email: ''
      }
    }
  },
  methods: {
    async signUp() {
      const { username, password, email } = this.form
      await Auth.signUp({
        username, password, attributes: { email }
      })
      this.formState = 'confirmSignUp'
    },
    async confirmSignUp() {
      const { username, authCode } = this.form
      await Auth.confirmSignUp(username, authCode)
      alert('successfully signed up! Sign in to view the app.')
      this.toggle()
    }
  }
}
</script>

<style>
.formcontainer {
  display: flex;
  flex-direction: column;
  width: 500px;
  margin: 0 auto;
}
.input {
  margin-bottom: 7px;
  height: 38px;
  border: none;
  outline: none;
  border-bottom: 2px solid #ddd;
  font-size: 20px;
}
.button {
  height: 45px;
  border: none;
  outline: none;
  background-color: #dddddd;
  margin-top: 8px;
  cursor: pointer;
  font-size: 18px;
}
.button:hover {
  opacity: .7
}
</style>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/SignUp.vue

  1. We have two separate forms - one for signing up & one for confirming sign up (MFA confirmation)
  2. We have a formState Boolean that we will use to toggle between the two forms.
  3. We have a form property on our data object that will keep up with the username, password, & email when a new user signs up.
  4. The signUp method calls the Amplify Auth.signUp method, passing in the form properties.
  5. The confirmSignUp method calls the Amplify Auth.confirmSignUp method, passing in the username & authCode. Once the user has successfully signed up we toggle the view to show the SignUp component.

Finally, let's take a look at the SignIn component. This component is very similar to SignUp in the sense that it has a form & calls a method on the Amplify Auth class.

src/components/SignIn.vue

// src/components/SignIn.vue
<template>
  <div>
    <h2>Sign In</h2>
    <div class='formcontainer'>
      <input placeholder="username" v-model='form.username' class='input' />
      <input placeholder="password" type='password' v-model='form.password' class='input' />
      <button v-on:click='signIn' class='button'>Sign In</button>
    </div>
  </div>
</template>

<script>
import { Auth } from 'aws-amplify'
export default {
  name: 'home',
  data() {
    return {
      form: {
        username: '',
        password: ''
      }
    }
  },
  methods: {
    async signIn() {
      const { username, password } = this.form
      await Auth.signIn(username, password)
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Details of src/components/SignIn.vue

  1. We have a form that allows the user to sign in
  2. We sign the user in calling the Amplify Auth.signIn method.
  3. In App.vue, we are listening for the signIn event, and the user will be routed to the Profile route after successfully signing in.

Testing it out

Part 2 of our application is finished so let's try it out!

~ npm run serve
Enter fullscreen mode Exit fullscreen mode

You should now see your app load with the new sign up / sign in forms we created.

Next Steps

The Amplify Auth class has over 30 different methods on it, including things like forgotPassword, setPreferredMFA, & signOut. Using these methods you can continue tailoring your authentication flow to be more robust.

The styling we used was minimal in order to keep this already long blog post from being too verbose, but since you can have full control over the authentication flow you can style it as you'd like.

Amplify authentication also supports federated sign in from providers like Facebook, Twitter, Google and Amazon. To learn more check out the documentation here.

Conclusion

To view the final repo & source code, click here.

To learn more about Amplify check out the documentation here.

Also check out the Awesome AWS Amplify Repo for more tutorials & starter projects.

My Name is Nader Dabit. I am a Developer Advocate at Amazon Web Services working with projects like AWS AppSync and AWS Amplify. I also specialize in cross-platform application development.

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