Session 10.2: "Secure" project (Spring Boot + VueJs) - Auth Service (JWT) for Microservice-based Systems
1. Clone the following repository
$ git clone https://github.com/M-Gharib/ESI-W10.2.git
2. Project setup - install dependencies
$ npm install
3. Run the frontend (VueJs project)
$ npm run serve
4. Run the backend projects (Spring Boot): Services are located within the \server
directory, run them one by one
Note: If you want to create the front-end project (VueJs) from scratch, please refer to the material of week 9 (Session 9.3: Vue.js - CRUD). You just need to install jwt-decode
, after creating the VueJs project, as follows
$ git npm install jwt-decode
5. The discovery server should run on port 8761 as defined in application.properties
. Visit http://localhost:8761/, and checked if all the services have registered themselves with the discovery server.
Backend (Spring Boot Services)
Our backend (all services) provides many request handlers (functions), but we will consider four of them, which are listed in the following table. The first 3 request handlers are provided by the Authentication service and the last is provided by the Product service.
Method | URI | Action |
POST | http://localhost:8080/api/auth/signup | Create/register a new user |
POST | http://localhost:8080/api/auth/login | Login a registered user |
GET | http://localhost:8080/api/auth/authenticate | Authenticate a user |
GET | http://localhost:8080/api/products | Fetch and present all products for authenticated users |
There are four services discovery-server
, product-service
, api-gateway
, and Authentication service
. The first two microservices were used before in this course, i.e., there are no modifications in them. The api-gateway service
and Authentication service
have been modified.
6. Concerning the Authentication service
, there are a few changes with respect to the previous week's project. The code is fully annotated to facilitate your comprehension, but your teacher will describe it to you. In particular, the main changes are in:
- In
UserController.java
, the@GetMapping("/authenticate")
receives the jwt token from the client, tries to validate it (depending on thevalidateToken(String token)
function), and returns the result. - In
JwtService.java
, thevalidateToken(String token)
function takes the token as an input, tries to validate it, and returns a boolean response concerning the result of validation. - We have also made some modifications to enable the
Authentication service
to register itself with thediscovery-server
, i.e., adding the@EnableDiscoveryClient
annotation before the main function, adding the required properties to itsapplication.properties
file, and adding the Eureka Discovery Client dependency to itspom.xml
7. Concerning the api-gateway service
, there are the following modifications to enable the auth for the microservices architecture.
filter/AuthenticationFilter.java
, defines a Gateway authentication filter, that intercepts and requests for protected endpoints and validates whether they contain tokens and whether these tokens are valid.filter/RouteValidator.java
, defines a set of routes that are exempt from the filter, i.e., they are not subject to the filter check. As you can see, these routes ("api/auth/authenticate", "api/auth/signup", and "api/auth/login") are the ones used for registering, login in, and authenticating a user, i.e., a user is not required to be authenticated to reach them.util/JwtUtil.java
, contains support functions/information for validating the token.
Note that the main idea of the AuthenticationFilter.java
and util/JwtUtil.java
is verifying the token the same way the Authentication service
do, and if a request pass such validation, it continue to its intended destination. Otherwise, it will be blocked.
Another modification in the api-gateway service
is adding the following properties to its application.properties
, which will solve the CORS issue as all requests will pass via the gateway.
... ############# You need these properties to avoid getting the cors error spring.cloud.gateway.default-filters=DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=* spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=* spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=* ...
A final modification in the api-gateway service
is applying the gateway filter to the routes that we want to protect, which we do for the product-service
.
... ####### product-service ####### ... ## We are applying the filter to the product-service route spring.cloud.gateway.routes[1].filters[0]= AuthenticationFilter ...
Frontend (VueJs)
The frontend application, when started, tries to visit the Home page, which is protected. Accordingly, the user is redirected to the login page. If the user is registered/signed up, she/he can enter her/his credentials. If the credentials are correct, she/he will receive a jwt token that grants her/him access to the Home page.
If the user is not registered, she/he can register via the signup page. Similarly, she/he will receive a jwt token that grants her/him access to the Home page after signing up. In both cases, the jwt token is saved in the local storage to be used when needed.
The Home page checks the role of the logged-in/signed-up user, then, fetches and presents the content related to the specified role. In short, the jwt token is fetched and added to the header of the request for getting the corresponding content.
In our example, both types of users (any authenticated user) fetch the same data (all products from the product service), but each of them is provided with a different interface. More specifically:
- An ADMIN can visualize, modify, delete, and also add new products.
- A USER can only visualize the products.
After successful login/signup, the Home page will show a button that allows the user to log out. In particular, when the log-out button is clicked on, the jwt that is saved in the local storage is deleted and the user is redirected to the Home page.
A simplified representation of the "workflow" of the application, and the sign-in, login, authenticate, fetch products, and logout workflow are shown in the following two figures.
The frontend application consists of several views and an auth.js file. Your teacher will describe them to you, yet we briefly describe each of them here:
SignUp.vue
contains a form that enables a user to enter his credentials (username, password, and roles). Note that the role is not a usual part of such form, which is usually assigned either a default role (e.g., a user, a customer) or set by the system administrator. We are allowing setting the role in the signup process for demonstrating how the auth/auth works. The entered credentials are sent to the server when the user press on the signup button. The server sends a jwt token as a response. The returned token is decoded, the role is abstracted from it, and saved in the local storage. Then, the user will be redirected to the Homepage, which will check whether the user can be authenticated, and since she/he has a valid token, she/he will be granted access.LogIn.vue
contains a form that enables a user to enter his credentials (username and password). The entered credentials are sent to the server when the user press on the login button. The server checks the credentials and if they are valid, it sends a jwt token as a response. The returned token is decoded, the role is abstracted from it, and saved in the local storage. Then, the user will be redirected to the Homepage, which will check whether the user can be authenticated, and since she/he has a valid token, she/he will be granted access.HomeView.vue
contains two simple templates dedicated to the two different roles a user can have ('ADMIN' and 'USER'). After a user is authenticated, its role will be specified, and based on such role one of these templates will be shown. On mount, the content of the template will be fetched from a protected end-point, namely the product's service.HomeView.vue
also contains a logout button that will be shown to users that are authenticated, which enables them to log out.AboutView.vue
the default about view page.auth.js
includes the following essential functions:authenticated
checks if there is a jwt token saved in the local storage, then, sends it to the server to be validated. If the token is valid, the server returns a positive response and the user is authenticated. Otherwise, the user cannot be authenticated.hasARoleOf
checks and returns the role of the logged/signed-in user, the role is abstracted from the returned or stored jwt token.logout
log the user out by removing/deleting the jwt that is saved in the local storage.
How we are controlling the user access in the Frontend (VueJs)?
The following snippet in router/index.js
checks whether the user that is trying to visit the home page is authenticated. Accordingly, he/she might be granted access or redirected to the login page.
//router/index.js ... import auth from "../auth"; ... const routes = [{ path: "/", name: "home", component: HomeView, beforeEnter: async(to, from, next) => { let authResult = await auth.authenticated(); if (!authResult) { next('/login') } else { next(); } } }, ...
Admin interactions with the backend
As mentioned earlier, an ADMIN can visualize, modify, delete, and also add new products. Unlike what we did in week 9, the products service
is a protected endpoint in this example. Accordingly, communicating with it requires including a valid token in each request, otherwise, the API Gateway will reject the request. We will cover only one of these requests as all of them works in almost the same way.
In the following snippet, the addProduct(product)
function is using the passed product
object as a body to a POST request to the backend (products service
). Pay particular attention to the token that is included in the header of the request.
Note: the Aproduct
object is passed to the addProduct(Aproduct)
function when the admin clicks the add
button, where the Aproduct is defined within the data section, and the values of the Aproduct attributes are obtained from the fields of the Add product form. Check the v-model
we are using for each of these values, which enables a two-way data binding on form input elements.
//HomeView.vue ... data: function() { return { ... Aproduct: { id: "", code: "", name: "", price: 0, description: "", }, token: localStorage.getItem('jwtToken') } ... addProduct(product) { fetch(`http://localhost:8080/api/products`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}` }, body: JSON.stringify(product), }) .then(() => { this.$router.push("/") window.location.reload(); }) .catch((e) => { console.log(e); }); }, ...
Changing the default port of VueJs
VueJs runs on port 8080 by default, but we are using the same port for our API Gateway service/ Therefore, we have changed the VueJs port to 9090. Check vue.config.js
, which is located in the main directory to see how we changed the port.
//vue.config.js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, devServer: { port: 9090 } })
Another way to change the port, is specifying it when you run your VueJs application, as follows:
$ npm run serve -- --port 9090