Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 9 – Security – Vue part)

Hi again! So, we finished the tutorial on building fintech app using Spring Boot and Vue.js. And this is the last part. Last time we touched the topic of security and implemented token authentication in Spring Boot. This time we will finish it and do security on client side (e.g. in Vue). We also will cover such topics as local storage and navigation guards.

What we will do in this part

In this tutorial we will implement frontend logic for signup, login and token validation. We will start from doing AuthManager that is going to abstract these actions from Vue components, define layouts and finally connect everything with backend using Axios.

Table of contents

Step 1. Create AuthManager

This component has a function to to abstract authentication logic from Vue components. Namely we need to perform following actions:

  1. Check that user is authenticated – e.g has obtained a token
  2. Save user credentials (token and ID) in local storage after login/signup was succesful
  3. Retrieve user credentials from local storage when we need them in components – for example we need to attach token as Authorization header to requests that call protected paths
  4. Do logout – clear all credentials from local storage

Create a new file auth/AuthManager.js. This is a pure JS code, no Vue here 🙂 Here is code snippet that demonstrates implementations of aforesaid functionality:

export function getUserId() {
    let userId = localStorage.getItem('userId')
    return userId
}

export function getAuthConfig() {
    let token = localStorage.getItem('token')
    let data = {headers: {'Authorization': token}}
    return data
}

export function saveCredentials(data) {
    localStorage.setItem('token', data.token)
    localStorage.setItem('userId', data.userId)
}

export function isAuthenticated() {
    let userId = localStorage.getItem('userId')
    let token = localStorage.getItem('token')
    if (userId !== null && token !== null){
        return true
    } else {
        return false
    }
}

export function logout() {
    localStorage.removeItem('token')
    localStorage.removeItem('userId')
}

As you note, here we provided all required actions:

  • isAuthenticated function checks that credentials do present and returns true or return false if no credentials presented. NB that actual validity of user token is asserted on backend layer. Client only can verify if they present at all (e.g. user sucessfully underwent a login process)
  • logout function clears local storage from token and userId. BTW, it is also possible to use just hard localStorage.clear() to delete everything. Also, I assume that there could be something useful
  • saveCredentials accepts TokenResponse from server and stores token and userId
  • Two remaining functions – getUserId and getAuthConfig are tiny wrappers to provide for Vue components more “nice way” to get this data. Although we can directly call local storage from components, these wrappers are worked as Java getters and abstract an actual way that we store credentials. This is especially important, if you use other patterns to keep app’s data.

Step 2. Create signup component

Before we can login, we need to create a user. In this section we will do a signup component.

Step 2.1. Define a layout

First thing first is to define a template for this component. Create a new Vue component SignupView.vue. This code snippet below implements a signup page layout:

<template>
    <div class="login-content">
        <div class="box">
            <h2 class="title is-2">Create a new account</h2>
            <b-field label="Email">
                <b-input type="email" 
                v-model="signupRequest.email"
                validation-message="Should be a valid email address"></b-input>
            </b-field>
            <b-field label="Your name">
                <b-input v-model="signupRequest.name"></b-input>
            </b-field>

            <div class="columns">
                <div class="column is-one-half">
                    <b-field label="Password">
                        <b-input type="password" v-model="signupRequest.password"></b-input>
                    </b-field>
                </div>

                <div class="column is-one-half">
                    <b-field label="Confirm password">
                        <b-input type="password" v-model="passwordRepeat"></b-input>
                    </b-field>
                </div>
            </div>

            <div class="columns">
                <div class="column is-one-third">
                    <b-button type="is-primary" 
                    v-on:click="doSignup">Create an account</b-button>
                </div>
                <div class="column is-two-thirds">
                    <router-link :to="{name: 'login'}">I have an account</router-link>
                </div>
            </div>
        </div>
    </div>
</template>

Here is how it looks like in real life:

Image 1. Signup view

What do we have here? First we have input fields for user’s email, name and password – this corresponds to SignupRequest, accepted by server. We also have two password inputs, as we need to assert that user enters password correctly. Finally, we have a login router link, that brings user to login page (although, we will implement it in the next step).

Step 2.2. Match passwords

Let define a data model for this component. We need here a SignupRequest object as well a value for passwordRepeat that we will test for similarity with SignupRequest.password:

//...
data() {
    return {
        signupRequest: {
            email: '',
            password: '',
            name: ''
        },
        passwordRepeat: ''
    }
}

Than, let create a method to test that passwords match:

validatePassword() {
    return this.passwordRepeat === this.signupRequest.password
}

This method just validates that passwords are equal, however you can develop this idea and add more validation: for instance that password has different type of characters.

Step 2.3. Perform signup API call

To start, we need to import Axios and saveCredentials function from AuthManager.js:

<script>
import {AxiosClient} from '@/http/AxiosClient.js'
import {saveCredentials} from '@/auth/AuthManager.js'

//..

Then, we can implement doSignup method that will call signup REST API and in case of success will store credentials in local storage. Finally we also need to redirect user in app, for example to the list of bills:

doSignup() {
    let validation = this.validatePassword()
    if (validation) {
        AxiosClient.post(`auth/signup`, this.signupRequest)
        .then(res=>{
            saveCredentials(res.data)
            this.$buefy.toast.open('Account created')
            this.$router.push({name: 'invoices-list'})
        })
        .catch(err=>{
            console.log(err)
            this.$buefy.toast.open({
                message: 'Something went wrong! We cannot create an account!',
                type: 'is-danger'
            })
        })
    } else {
        this.$buefy.toast.open('Passwords dont match!')
    }
}

Step 3. Create login component

This component will implement login logic in our Vue app. Basically, it is similar to certain extent with the previous one, rather does not have password validation.

Step 3.1. Define a layout

Create a new component called LoginView.vue. As usual we will start with a template definition:

<template>
    <div class="login-content">
        <div class="box">
            <b-field label="Email">
                <b-input type="email" 
                v-model="loginRequest.email"
                validation-message="Should be a valid email address"></b-input>
            </b-field>
            <b-field label="Password">
                <b-input type="password" v-model="loginRequest.password"></b-input>
            </b-field>
            <div class="columns">
                <div class="column is-one-third">
                    <b-button type="is-primary" v-on:click="doLogin">Login</b-button>
                </div>
                <div class="column is-two-thirds">
                    <router-link :to="{name: 'signup'}">I don't have an account</router-link>
                </div>
            </div>
        </div>
    </div>
</template>

As you can see here we have two fields and also a router link to move user to signup component, if there is no account. This is how login view looks like:

Image 2. Login view

Step 3.2. Do login API call

The only method here to implement is to call login REST API to obtain token. Start with importing of http client and saveCredentials function:

import {AxiosClient} from '@/http/AxiosClient.js'
import {saveCredentials} from '@/auth/AuthManager.js'
//

Next, let define a data model for [LoginRequest]():

data() {
    return {
        loginRequest: {
            email: '',
            password: ''
        }
    }
}

Finally, let create a doLogin method:

methods: {
    doLogin() {
        AxiosClient.post(`auth/login`, this.loginRequest).then(res => {
            saveCredentials(res.data)
            this.$buefy.toast.open('Logged in successfully')
            this.$router.push({name: 'bills-list'})
        }).catch(err => {
            console.log(err)
            this.$buefy.toast.open({
                message: 'Something went wrong',
                type: 'is-danger'
            })
        })
    }
}

Step 4. Protect routes with navigation guards

As we created an authentication flow in our application, we need to protect secured routes, that only authenticated users can access them. Vue.js performs this with navigation guards. From a technical point of view, guard is a special function we attach to route in router. There are two types of guards: global (for all routes) and applied only for specific route. Here we will utilize second approach.

Basically, guard is function with three arguments:

beforeEnter (to, from, next) => {
    //..
}

Let see what is what here:

  • to = the route, where user does navigate, e.g. target. This is an object
  • from = the router, where user comes from, e.g. origin. This is an object
  • next = this is the function that actually does solve can user access this route or not. There are several possible situations – let observe them below

So how we can procceed in route?

  1. Just call next(): allow to user to go this route
  2. Redirect user with next(path), where we can put named route like {name: 'login'}, for instance is user is not authenticated
  3. next(false) will reset user to from destination

In our case, we need to add to routes a guard that checks that user does have token and if so to go this route (call next()) or to redirect to login page (next({name: 'login'})). However, this does not applied for login and signup routes.

First import isAuthenticated function:

import {isAuthenticated} from '@/auth/AuthManager.js'

Then, apply guards to routes:

const routes = [
  {
    path: '/companies/new',
    name: 'companies-new',
    component: () => import('../views/CompaniesNewView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/companies/list/:type',
    props: true,
    name: 'companies-list',
    component: () => import('../views/CompaniesListView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/bills/new',
    name: 'bills-new',
    component: () => import('../views/BillsNewView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/bills/list',
    name: 'bills-list',
    component: () => import('../views/BillsListView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/bill/:id',
    props: true,
    name: 'bill-one',
    component: () => import('../views/BillOneView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/invoices/new',
    name: 'invoices-new',
    component: () => import('../views/InvoicesNewView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/invoices/list',
    name: 'invoices-list',
    component: () => import('../views/InvoicesListView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/invoice/:id',
    props: true,
    name: 'invoice-one',
    component: () => import('../views/InvoiceOneView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/subscriptions',
    name: 'subscription',
    component: () => import('../views/SubscriptionView.vue'),
    beforeEnter: (to, from, next) => {
      if (isAuthenticated()){
        next()
      } else {
        next({name: 'login'})
      }
    }
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('../views/LoginView.vue')
  },
  {
    path: '/signup',
    name: 'signup',
    component: () => import('../views/SignupView.vue')
  }
]

Congratulations! You just finished the tutorial on full stack development with Spring Boot and Vue.js. You can access source code on my github:

Copy link
Powered by Social Snap