How to Build Production-ready Vue Authentication
Nader Dabit
Posted on March 4, 2019
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
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
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
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
Next, we'll need to configure the CLI. To do so, run the following command:
~ amplify configure
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
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
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 runamplify 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:
- Part 1 - Using the preconfigured
amplify-authenticator
component from AWS Amplify Vue to quickly get our auth flow up & running. - 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')
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
Details of src/router.js
- We import Vue & VueRouter
- We import the components we'll be using in our routes
- We define an array of routes. We add an additional meta property to specify the routes requiring authentication by using a Boolean named
requiresAuth
. - We create the router variable
- 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>
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:
- Displaying the navigation links
- Rendering the router
- 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>
Details of src/components/App.vue
- We use the
amplify-sign-out
component to render a sign out button if the user is signed in. - We create a Boolean called
signedIn
& set it to false when the app loads - In the
beforeCreate
lifecycle method we listen for theauthState
event by using theHub
API. If we detect a sign in, we redirect them to view their profile & setsignedIn
to true. If we detect a sign out, we redirect them to the/auth
route & setsignedIn
to false. - When the app loads, we also call
Auth.currentAuthenticatedUser
to check whether or not the user is signed in & set thesignedIn
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>
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>
src/components/Protected.vue
// src/components/Protected.vue
<template>
<h1>Hello from protected route!</h1>
</template>
<script>
export default {
name: 'protected',
}
</script>
Testing it out
Part 1 of our application is finished, so let's test it out:
~ npm run serve
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
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>
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>
Details of src/components/SignUp.vue
- We have two separate forms - one for signing up & one for confirming sign up (MFA confirmation)
- We have a
formState
Boolean that we will use to toggle between the two forms. - We have a form property on our data object that will keep up with the
username
,password
, &email
when a new user signs up. - The
signUp
method calls the AmplifyAuth.signUp
method, passing in the form properties. - The
confirmSignUp
method calls the AmplifyAuth.confirmSignUp
method, passing in theusername
&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>
Details of src/components/SignIn.vue
- We have a form that allows the user to sign in
- We sign the user in calling the Amplify
Auth.signIn
method. - 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
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.
Posted on March 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.