Setting up the vue application

Although we could work on an existing application, I will propose to start with a brand new application (to reduce the risk of surprises). It will be a good exercise for you to transfer the components introduced here into your application.

Therefore, we will start by creating a new vue application with vue-cli as we did a few weeks ago. In a terminal window, you can run the following command (please select the router component in the configuration menu):

vue create secfront
cd secfront
npm install --save buefy axios

Note that we have already added buefy for handling reactive user experience and axios for REST interactions. Now, open your project into a code editor (e.g. MS code). If you are planning to use some icons in the app’s UI, add the reference to the corresponding library in public/index.html. For instance, to use Google material icons, you should add the following line in the header section of the aforementioned file:

<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">

To complete the setup, we have to import buefy and register it upon vue runtime. The latter can be done by adding the following lines in file main.js.

import Buefy from 'buefy'
import 'buefy/lib/buefy.css'

Vue.use(Buefy)

Adding a login page

Let us start by adding a vue component to handle the login of users into our frontend application. I propose you to create a folder auth inside the folder components. Then, create a file called Login.vue and copy there the following snippet.

components/auth/Login.vue
<template>
  <div>
    <div class="column is-half is-offset-one-quarter">
      <b-field label="Username">
        <b-input v-model="username"></b-input>
      </b-field>
      <b-field label="Password">
        <b-input type="password" v-model="password"></b-input>
      </b-field>
      <a class="button is-info is-fullwidth" @click="login">Log in</a>
    </div>
  </div>
</template>
<script>
// import auth from "./auth";
export default {
  name: 'Login',
  data: function() {
    return {
      username: "",
      password: ""
    }
  },
  methods: {
    login: function() {
      console.log(`${this.username}, ${this.password}`);
      // auth.login(this, this.username, this.password, "/");
    }
  }
}
</script>

Now, let us add a route to make the login page accessible to the user. To this end, we would need to change the file router.js as follows:

import Login from './components/auth/Login.vue'

export default new Router({
  routes: [
      { path: '/login', name: 'login', component: Login},
      // leave the rest as before
  ]
})

Note that the above will not change the application’s navigation component, but paht /login will now be recognized by the application.

Start your vue application and play with it a little bit. You can use the command npm run serve to that end.

Complete the authentication round trip

Right now, we have a visual component to let the users provide their credentials. Different from JWT, where the next step was to submit the credentials to the backend for authentication, we would need to encode the credentials on the frontend using Base64 and use the encoded value as the security token in every REST interaction.

What I would propose you is to keep the security token in local store and to remove it only when the user logs out. This simple scheme would allow us to simulate a web user session.

In addition to this, I propose you to use the login action as an opportunity to query the backend to know the roles played by the user and, with this information, implement a simple role-based access control. To implement this, let me first introduce the method that we will use in the backend. Create a controller class and copy there the following code:

@RestController
@CrossOrigin
public class AuthenticationController {
    @GetMapping("/api/authenticate")
    public List<String> authenticate() {
        Object principal = SecurityContextHolder
                .getContext()
                .getAuthentication().getPrincipal();
        List<String> roles = new ArrayList<>();
        if (principal instanceof UserDetails) {
            UserDetails details = (UserDetails) principal;
            for (GrantedAuthority authority: details.getAuthorities())
                roles.add(authority.getAuthority());
        }
        return roles;
    }
}

Note that the method authenticate would be reachable if and only if the request include the security token in the headers. With such information spring security runtime can determine the username and also query the database for the corresponding "authorities" (jargon for referring to the user’s roles). What the method above is to retrieve the roles associated with the user and return them as a list.

We are now ready to add the javascript code for authentication. Copy the following snippet into file components/auth/auth.js.

import axios from "axios";

export default {
  user: { roles: [], username: "", authenticated: false },
  login: function (context, username, password, redirect) {
    let token = btoa(username+":"+password);

    axios.get("http://localhost:8080/api/authenticate", {headers: {'Authorization': `Basic ${token}`}})
      .then(response => {
        this.username = username;
        this.user.roles = response.data;
        this.authenticated = true;
        window.localStorage.setItem('token-'+this.username, token);
        if (redirect)
          context.$router.push({path: redirect});
      })
      .catch(error => {
        console.log(error);
      });
  },
  hasAnyOf: function(roles) {
    return this.user.roles.find(role => roles.includes(role));
  },
  logout: function() {
    window.localStorage.removeItem('token-'+this.username);
    this.user = { roles: [], username: "", authenticated: false };
  },
  authenticated: function() {
    return this.user.authenticated;
  },
  getAuthHeader: function() {
    return {
    'Authorization': window.localStorage.getItem('token-'+this.username)
    }
  }
}

(If you took the course on Agile software development, you will find that the code above is quite similar to the one used there). The code above defines a property "user" that keeps the user session’s information: the username, the set of roles associated with the user and a flag indicating if the user has been authenticated. The method login uses a REST interaction with the backend to retrieve the roles associated with the user that is trying to login. Of course, if the credentials are wrong, the information about the user will be kept as they were (usually referring to an nonexisting session). However, if the authentication succeeds, we will store the information about the session and redirect to the path that was sent as reference.

The statement btoa(username+":"+password) is the one that computes the Base64 encoding for the concatenation of the credentials. Note that we will copy that value into the headers as part of the REST interaction (see {headers: {'Authorization': `Basic ${token}}}`) and later into local storage for future interactions, i.e. window.localStorage.setItem('token-'+this.username, token).

The file also includes some other convenience functions:

  • Function logout cleans up the user session’s information (including local storage).

  • Function hasAnyOf takes as input an array with roles as strings and checks if there is at least one matching with the roles of the currently logged in user.

  • Function authorized returns true if the application has successfully checked the credentials of the user and his/her session is still valid (he/she has not logged out).

  • Function getAuthHeader is meant to be used in parts of the code where we need to perform a REST interaction. The function would return a javascript object containing the configuration of the HTTP headers.

To try out, you would only need to comment out the lines in Login.vue that would enable the call to auth.login.

Restricting the access to content

With all the above, we have now the required infrastructure to implement the authentication process upon the backend. Since we have the information about the roles played by the logged in user, we will now implement a simple access control.

Firstly, we will secure the access to web pages using the router component from Vue. Please replace fully the content of file router.js from your project with the following snippet.

router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Login from './components/auth/Login.vue'
import auth from './components/auth/auth.js'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      beforeEnter: (to, from, next) => {
        if (!auth.authenticated()) {
          next({path: '/login'});
        } else {
          next();
        }
      }
    },
    { path: '/login', name: 'login', component: Login},
    { path: '/logout', name: 'logout', component: Login,
    beforeEnter: (to, from, next) => {
      auth.logout();
      next({path: '/login'});
    }
  },
  ]
})

The key section in the code above is the use of the "hook" function beforeEnter. Such function is executed whenever we want the application to move to a certain page (according to the value of path). The function passes three parameters: to that represents the target page, from that represents the source page, and next which is a callback function which can be used to perform a redirection.

As you can see, we have guarded the access to the home page, i.e. it will be accessible to a user if and only if the user has been authenticated. Note that we use the code from auth.js to verify if the user has been authenticated. If the user is not authenticated, the application is redirected to the login page.

Note that we are using a very similar approach to implementing the functionality of logout. The attempt to logout calls first auth.logout() to clear all the user’s information (including the removal of the token from local storage). It then redirects the application to the login page.

Let us now update the application’s navigation bar, which is specified within file App.vue. What I propose you is to change the reference to about (generated by vue-cli) to refer now to logout. You can safely change the template included in App.vue with the following snippet.

App.vue
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/logout">Logout</router-link>
    </div>
    <router-view/>
  </div>
</template>

Finally, let me illustrate how we can use the function auth.hasAnyOf() to control the access to certain parts of the user interface. To that end, I propose you to use vue’s directive v-if with a call to auth.hasAnyOf(), as illustrated in the following code:

Home.vue
<template>
  <div class="home">
  <div v-if="checkRoles(['ROLE_USER1'])">
    Hello User1
  </div>
  <div v-if="checkRoles(['ROLE_USER2'])">
    Hello User2
  </div>
  </div>
</template>

<script>
import auth from '@/components/auth/auth.js';

export default {
  name: 'home',
  methods: {
    checkRoles: function(roles) {
      return auth.hasAnyOf(roles);
    }
  }
}
</script>

Please note that we are specifying not only one role but an array of them. The function auth.hasAnyOf() will check if there is at least one role matching between the list specified in the v-if directive and the list of roles stored by auth. If the matching is positive, one part of the UI will be render while the other one will remain hidden.

Of course, auth.hasAnyOf() can be used in other places (e.g. in the router configuration to check, not only that the user has been authenticated, but if the user complies with the set of roles explicitly specified).

Authentication on integration flows and restTemplate

In this section, I will propose you an approach to add the security tokens to the REST interactions stemming from HTTP outbound gateways (cf. integration flows) and/or restTemplate. The approach assumes that the credentials are read from a properties files instead of hard coding them in the application.

Let us start by creating a property file called credentials.properties within the folder resources on your project. Copy there the following information and adapt it if required.

authority.rentit1=localhost:8080
authorization.rentit1=Basic dXNlcjE6dXNlcjE=
authority.rentit2=localhost:8090
authorization.rentit2=Basic dXNlcjI6dXNlcjI=

As you can see, I am assuming that we have to RentIt applications that are running in localhost in different ports. For each one of these RentIts we are going to store the string that we usually add to the headers of the HTTP request.

Now, let us figure out how to read the content of the file. Well, the solution is given in the following snippet. Copy the code into the corresponding java file.

@Configuration
@EnableConfigurationProperties
@PropertySource("classpath:credentials.properties")
@ConfigurationProperties
@Getter
public class CredentialsService {
    Map<String, String> authority = new HashMap<>();
    Map<String, String> authorization = new HashMap<>();
}

As you can see the class is annotated with a reference to the property file and includes two has maps to store the values of authority and authorization specified in the property file. In fact, this is all we need. Let me show it how can we use it. To that end, replace the code of the entry class on your project with the following one:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
        CredentialsService service = ctx.getBean(CredentialsService.class);

        System.out.println(service.getAuthority());
        System.out.println(service.getAuthentication());
    }
}

If you run the new code, you will see that the main function will print out the content of the properties file.

Now, we will need to adapt a class that has remained hidden to us and that is responsible for creating an empty HTTP request, whenever we want to start a REST interaction. Copy the following code to your project.

@Configuration
public class HttpRequestAdapter {

    static class MyHttpRequestFactory extends SimpleClientHttpRequestFactory {
        @Autowired
        CredentialsService credentials;

        @Override
        public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
            ClientHttpRequest request = super.createRequest(uri, httpMethod);
            for (String key: credentials.getAuthority().keySet()) {
                if (credentials.getAuthority().get(key).equals(uri.getAuthority())) {
                    request.getHeaders().add("Authorization", credentials.getAuthorization().get(key));
                    break;
                }
            }
            return request;
        }
    }

    @Bean
    public ClientHttpRequestFactory requestFactory() {
        return new MyHttpRequestFactory();
    }
}

The key part of the code above is the line ClientHttpRequest request = super.createRequest(uri, httpMethod);, where we create the HTTP request. Note that we are passing the URI. The idea is then to compare the "authority" part of the URL with the one on the credentials property file, if we find a match, then we add the header Authorization with the corresponding security token.

Let us now try out the HTTP request factory above. Change the code of the main function on the entry class with the following snippet:

public static void main(String[] args) {
    ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
    CredentialsService service = ctx.getBean(CredentialsService.class);
    ClientHttpRequestFactory requestFactory = ctx.getBean(ClientHttpRequestFactory.class);

    System.out.println(service.getAuthority());
    System.out.println(service.getAuthorization());

    RestTemplate restTemplate = new RestTemplate(requestFactory);

    System.out.println(restTemplate.getForObject("http://localhost:8080/api/services/resource1", String.class));
}

Well, as you can see, we would need to get access to the bean ClientHttpRequestFactory (usually with an @Autowired) and we have to use it when we initialize the restTemplate. We would use something similar with an integration flow. Consider the following example:

@Bean
IntegrationFlow flow(ClientHttpRequestFactory factory) {
    return IntegrationFlows.from("reqChannel")
            .handle(Http.outboundGateway("http://localhost:8080/api/services/resource1")
                    .requestFactory(factory)
                .expectedResponseType(String.class)
                .httpMethod(GET))
            .get();
}