Developing an authentication system in Java+Tarantool
Tags: java, javascript, lua, programming
Published: 2022-02-21 12:01:10

Author: Alexander Goryakin


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a6qyyxhcpz78ns34ieru.png)
 

My name is Alexander, I am a software engineer in the architecture and pre-sale department at VK. In this article, I'm going to show you how to build an authentication system based on Tarantool and Java. In pre-sales, we often have to implement such systems. There are plenty of authentication methods: by password, biometric data, SMS, etc. To make it simple, I'll show you how to implement password authentication.

This article should be useful for those who want to understand the construction of authentication systems. I will use a simple example to demonstrate all the main parts of such an architecture, how they relate to each other and how they work as a whole.  
The authentication system verifies the authenticity of the data entered by the user. We encounter these systems everywhere, from operating systems to various services. There are many types of authentication: by login and password pair, with electronic signature, biometric data, etc. I chose the login-password pair as an example, as it's the most common and quite simple. And it also allows showing the basic features of Cartridge and Cartridge Java, with a fairly small amount of code. But first things first.


## Fundamentals of authentication systems


In any authentication system, you can usually identify several elements as follows:


-   *subject* that will undergo the procedure;
-   *characteristic* of the subject, its distinguishing feature;
-   *host of the authentication system*, who is responsible for it and controls its operation;
-   *authentication mechanism*, that is, the operating principles of the system;
-   *access control mechanism*, which grants certain access rights to a subject.


The authentication mechanism can be provided by the software that verifies the authenticity of the subject characteristics: a web service, an operating system module, etc. Most often, the subject characteristics must be stored somewhere, which means there must be a database, MySQL or PostgreSQL, for example.

If there is no existing software that allows you to implement an authentication mechanism according to certain rules, you have to write it by yourself. Among these cases, I can list authentication by several characteristics, with complicated verification algorithms, etc.


## What are Tarantool Cartridge and Cartridge Java?


Tarantool Cartridge is a framework for scaling and managing a cluster of multiple Tarantool instances. Besides creating a cluster, it also allows you to manage that cluster quite effectively, such as expanding it, automatically resharding it and implementing any role-based business logic.

To work with the cluster from an application, you need to use connectors”drivers for interaction with the database and the cluster using the iproto binary protocol. Tarantool currently has connectors for programming languages such as Go, Java, Python, to name a few. Some of them can only work with one instance of Tarantool, while others can work with entire clusters. One of those connectors is Cartridge Java. It allows you to interact with a cluster from a Java application. This brings up a reasonable question: why this particular language?


## Why Java?


I work in the architecture and pre-sales department, which means that we make pilot projects for customers from different areas of business. By a pilot project, I mean a prototype of a system, that will later be finalized and handed over to the customer. That is why our customers are mostly people who use programming languages that allow them to create full enterprise solutions. One of those is Java, so we chose Cartridge Java connector for this example.


## Why authentication process?


The next question that arises is the choice of a service on which we will demonstrate our technology. So why did we take authentication and not some other service? The answer is quite simple: this is the most common problem that people try to solve not only with Tarantool but with other databases as well.

Users encounter authentication in almost all more or less major applications. Most commonly, databases such as MySQL or PostgreSQL are used to store user profiles. However, using Tarantool here is most appropriate since it can handle tens of thousands of queries per second due to the fact that all data is stored in RAM. And if an instance crashes, it can recover rather quickly via snapshots and write-ahead logs.

Now let's get to the structure of our sample service. It will consist of two parts:


-   **Tarantool Cartridge application**, serving as a database;
-   **Java application**, providing an API for performing basic operations.


Let's start by looking at the first part of our service.


## Tarantool Cartridge application


This application will provide a small cluster of one router, two sets of storage replicas, and one stateboard.

**Router** is an instance with the **router** role. It is responsible for routing requests to storage. We're going to expand its functionality a little bit. I'll explain how to do it further below.

**Replica set** (storage replica set) refers to a group of N instances with the **storage** role, of which one is the master, and the rest are its replicas. In our case, these are pairs of instances that act as profile storage.

**Stateboard** is responsible for configuring the failover mechanism of the cluster in case of failure of individual instances.


### Creating and configuring an application


Let's create an application by executing


``` lua
$ cartridge create --name authentication
```


This will create "authentication" directory, containing everything you need to create a cluster. Let's define a list of instances in the *instances.yml* file:


``` lua
---
authentication.router:
  advertise_uri: localhost:3301
  http_port: 8081

authentication.s1-master:
  advertise_uri: localhost:3302
  http_port: 8082

authentication.s1-replica:
  advertise_uri: localhost:3303
  http_port: 8083

authentication.s2-master:
  advertise_uri: localhost:3304
  http_port: 8084

authentication.s2-replica:
  advertise_uri: localhost:3305
  http_port: 8085

authentication-stateboard:
  listen: localhost:4401
  password: passwd
```


Now we have to configure the roles.


### Configuring roles


For our application to work with the Cartridge Java connector, we need to create and configure new roles. You can do this by copying the *custom.lua* file and renaming the copies into *storage.lua* and *router.lua*, placing them into the *app/roles* directory, and then changing the settings in them. First, change the name of the role”the value in the `role_name` field”in the `return` statement. In *router.lua* the role will be `router` and in *storage.lua* it will be `storage`. Second, specify the corresponding role names in *init.lua* in the `roles` section of the *cartridge.cfg* file.

In order to work with Cartridge Java, we need to install the *ddl* module by adding `'ddl == 1.3.0-1'` to the `dependencies` section of the file with the *.rockspec* extension. And add the `get_schema` function to *router.lua* after that:


``` lua
function get_schema()
    for _, instance_uri in pairs(cartridge_rpc.get_candidates('app.roles.storage', { leader_only = true })) do
        local conn = cartridge_pool.connect(instance_uri)
        return conn:call('ddl.get_schema', {})
    end
end
```


Add the following to the `init` function:


``` lua
rawset(_G, 'ddl', { get_schema = get_schema })
```


In addition, add the following condition to the `init` function in *storage.lua*:


``` lua
 if opts.is_master then
        rawset(_G, 'ddl', { get_schema = require('ddl').get_schema })
 end
```


It means that we have to execute the `rawset` function on those storages which are masters. Now let's move on to defining the cluster topology.


### Defining a cluster topology and launching the cluster


Let's specify the cluster topology in the *replicasets.yml* file:


``` lua
router:
  instances:
  - router
  roles:
  - failover-coordinator
  - router
  all_rw: false
s-1:
  instances:
  - s1-master
  - s1-replica
  roles:
  - storage
  weight: 1
  all_rw: false
  vshard_group: default
s-2:
  instances:
  - s2-master
  - s2-replica
  roles:
  - storage
  weight: 1
  all_rw: false
  vshard_group: default
```


After establishing the instance configuration and topology, execute the commands to build and run our cluster:


``` lua
$ cartridge build
$ cartridge start -d
```


The instances that we defined in *instances.yml* will be created and launched. Now we can access `http://localhost:8081` in a browser to manage our cluster via GUI. All the created instances will be listed there. However, they are not configured or combined into replica sets as we described in *replicasets.yml* just yet. To avoid configuring instances manually, run the following:


``` lua
$ cartridge replicasets setup -bootstrap-vshard
```


If we check the list of our instances now, we'll see that the topology is now set up, that is, the instances have the appropriate roles assigned to them, and they are combined into replica sets:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zulfm4vo6ny7n2nwb8t5.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/krirffn5zzi8qyk213xr.png)
 
  

Furthermore, the initial bootstrapping of the cluster was performed, which resulted in a working sharding. And now we can use our cluster!


### Building a data model


Well, actually we can't make use of it just yet, since we don't have a proper data model to describe the user. Let's see, what do we need to describe the user? What kind of information about the user do we want to store? Since our example is quite simple, let's use the following fields as general information about the user:


-   `uuid`, user's unique identifier;
-   `login`, user's login;
-   `password`, the hash sum of the user's password.


These are the main fields that the data model will contain. They are sufficient for most cases when there are few users and the load is pretty low. But what happens when the number of users becomes immense? We would probably want to implement sharding, so we can distribute users to different storages, and those in turn to different servers or even different data centers. Then what field should we use to shard the users? There are two options, UUID and login. In this example, we're going to shard the users by login.

Most often, the sharding key is chosen so that a storage will contain records with the same sharding key, even if they belong to different spaces. But since there is only one space in our case, we can choose any field we like. After that, we have to decide which algorithm to use for sharding. Fortunately, this choice is not necessary because Tarantool Cartridge already has the *vshard* library, which uses a virtual sharding algorithm. To use this library, we need to add one more field to the data model, `bucket_id`. This field's value will be calculated based on the login field's value. And now we can describe our space in full:


``` lua
local user_info = box.schema.create_space('user_info', {
            format = {
                { name = 'bucket_id', type = 'unsigned' },
                { name = 'uuid', type = 'string' },
                { name = 'login', type = 'string' },
                { name = 'password', type = 'string' },
            },
            if_not_exists = true,
        })
```


To start using the space, we have to create at least one index. Let's create a primary index `primary` based on the `login` field:


``` lua
user_info:create_index('primary', {
            parts = { 'login' },
            if_not_exists = true,
        })
```


Since we are using vshard, we also need to create a secondary index based on the `bucket_id` field:


``` lua
user_info:create_index('bucket_id', {
            parts = { 'bucket_id' },
            if_not_exists = true,
            unique = false
        })
```


Now let's add a sharding key based on the `login` field:


``` lua
utils.register_sharding_key('user_info', {'login'})
```



### Performing migrations


We'll use the *migrations* module to work with spaces. To do this, add this line to the `dependencies` section of the file with the *.rockspec* extension:


``` lua
'migrations == 0.4.0-1'
```


To use this module, create a *migrations* directory in the application's root directory and put a *0001_initial.lua* file with the following contents there:


``` lua
local utils = require('migrator.utils')

return {
    up = function()
        local user_info = box.schema.create_space('user_info', {
            format = {
                { name = 'bucket_id', type = 'unsigned' },
                { name = 'uuid', type = 'string' },
                { name = 'login', type = 'string' },
                { name = 'password', type = 'string' },
            },
            if_not_exists = true,
        })

        user_info:create_index('primary', {
            parts = { 'login' },
            if_not_exists = true,
        })

        user_info:create_index('bucket_id', {
            parts = { 'bucket_id' },
            if_not_exists = true,
            unique = false
        })

        utils.register_sharding_key('user_info', {'login'})

        return true
    end
}
```


To create our space, we have to send a POST request to `http://localhost:8081/migrations/up`, such as this:


``` lua
$ curl –X POST http://localhost:8081/migrations/up
```


By doing so, we perform the migration. To create new migrations, add new files with names beginning with 0002-…, to the *migrations* directory and run the same command.


### Creating stored procedures


After constructing the data model and building the space for it, we need to create functions through which our Java application will interact with the cluster. Such functions are referred to as stored procedures. They are called on routers and they process the data by invoking certain space methods.

What kind of operations with user profiles do we want to perform? Since we want to use our cluster primarily as profile storage, it's obvious that we should have a function to create profiles. In addition, since this application is an example of authentication, we should be able to get information about the user by their login. And finally, we should have a function to update a user's information, in case a user forgets their password, for instance, and a function to delete a user if they want to delete their account.

Now that we have defined which basic stored procedures we want, it's time to implement them. The entire code for them will be stored in the *app/roles/router.lua* file. Let's start by implementing the user creation, but first we'll set up some auxiliary constants:


``` lua
local USER_BUCKET_ID_FIELD = 1
local USER_UUID_FIELD = 2
local USER_LOGIN_FIELD = 3
local USER_PASSWORD_FIELD = 4
```


As you can see from their names, these constants define the numbers of the corresponding fields in the space. These constants will allow us to use meaningful names when indexing the fields of the tuple in our stored procedures. Now let's move on to creating the first stored procedure. It will be named `create_user` and will receive UUID, username, and password hash as parameters.


``` lua
function create_user(uuid, login, password_hash)
    local bucket_id = vshard.router.bucket_id_mpcrc32(login)

    local _, err = vshard.router.callrw(bucket_id, 'box.space.user_info:insert', {
        {bucket_id, uuid, login, password_hash }
    })

    if err ~= nil then
        log.error(err)
        return nil
    end

    return login
end
```



1.  First, we use `vshard.router.bucket_id_mpcrc32` to calculate the `bucket_id` parameter, which will be used to shard our entries.
2.  Then we call the `insert` function from the space on the bucket with the calculated `bucket_id`, and pass a tuple consisting of `bucket_id`, `uuid`, `login` and `password_hash` fields to this space. This call is performed using the `vshard.router.callrw` call of the *vshard* library, which allows write operations to the space and returns the result of the function being called (and an error if it fails).
3.  Finally, we check if our function has been executed successfully. If yes — the data was inserted into the space — we return the user's login. Otherwise, we return `nil`.


Now let's create the next stored procedure, the one for getting information about the user by their login. This one will be named `get_user_by_login`. We will apply the following algorithm to it:


1.  Calculate the `bucket_id` by login.
2.  Call the `get` function for the calculated bucket via the `vshard.router.callbro` function.
3.  If a user with the specified login exists, then we return the tuple with information about the user, otherwise return `nil`.


Implementation:


``` lua
function get_user_by_login(login)

    local bucket_id = vshard.router.bucket_id_mpcrc32(login)

    local user = vshard.router.callbro(bucket_id, 'box.space.user_info:get', {login})
    return user
end
```


Besides authentication, it will also be helpful in updating and deleting user information.

Let's consider the case where the user decided to update their information, for example, their password. We're going to write a function named `update_user_by_login` that will accept the user's login and the new password's hash. Which algorithm should we use for that task? Let's start by trying to get the user's information via the `get_user_by_login` function we have implemented. If the user doesn't exist, we'll return `nil`. Otherwise, we'll calculate `bucket_id` by the user's login and call the `update` function for our space on the bucket with the calculated id. We'll pass the user's login and the tuple containing information about the field we need to update — the new password hash — to this function. If an error occurred during the update, then we will log it and return `nil`, otherwise we will return the tuple with the user's information. In Lua, this function will look like this:


``` lua
function update_user_by_login(login, new_password_hash)
    local user = get_user_by_login(login)

    if user ~= nil then
        local bucket_id = vshard.router.bucket_id_mpcrc32(user[USER_LOGIN_FIELD])

        local user, err = vshard.router.callrw(bucket_id, 'box.space.user_info:update', { user[USER_LOGIN_FIELD], {
            {'=', USER_PASSWORD_FIELD, new_password_hash }}
        })

        if err ~= nil then
            log.error(err)
            return nil
        end

        return user
    end

    return nil
end
```


And lastly, let's implement the function for deleting a user. It will be named `delete_user_by_login`. The algorithm will be somewhat similar to the update function, the only difference being that if a user exists in the space, the `delete` function will be called and the information about the deleted user will be returned, otherwise the function will return `nil`. This stored procedure's implementation goes as follows:


``` lua
function delete_user_by_login(login)

    local user = get_user_by_login(login)

    if user ~= nil then

        local bucket_id = vshard.router.bucket_id_mpcrc32(user[USER_LOGIN_FIELD])

        local _, _ = vshard.router.callrw(bucket_id, 'box.space.user_info:delete', {
            {user[USER_LOGIN_FIELD]}
        })

        return user
    end

    return nil

end
```



### What was done



-   We built an application.
-   Configured roles for it.
-   Set up a cluster topology.
-   Launched the cluster.
-   Described a data model and created migration logic.
-   Implemented stored procedures.


Now we can restart the cluster and start filling it with data. In the meantime, we'll move on to developing the Java application.


## Java application


The Java application will serve as an API and will provide the business logic for user authentication. Since it's an enterprise application, we will create it using the Spring framework. We are going to use the Apache Maven framework to build it.


### Setting up the connector


To set the connector, add the following dependency in the `dependencies` section of the *pom.xml* file:


``` lua
<dependency>
     <groupId>io.tarantool</groupId>
     <artifactId>cartridge-driver</artifactId>
     <version>0.4.2</version>
</dependency>
```


After that, we must update the dependencies. You can find the latest connector's version [**here**](https://github.com/tarantool/cartridge-java). After installing the connector, we need to import the necessary classes from `io.tarantool.driver` package.


### Connecting to the cluster


After setting up the connector, we need to create a class that will be responsible for its configuration and will connect the application to the Tarantool Cartridge cluster. Let's call this class `TarantoolConfig`. We will specify that it is a configuration class and that its parameters are defined in the *application-tarantool.properties* file:


``` lua
@Configuration
@PropertySource(value="classpath:application-tarantool.properties", encoding = "UTF-8")
```


The *application-tarantool.properties* file contains the following lines:


``` lua
tarantool.nodes=localhost:3301 # node list
tarantool.username=admin # user name
tarantool.password=authentication-cluster-cookie # password
```


They specify the values of the fields required to connect to the cluster. This is why the constructor of our class takes these parameters as input:


``` lua
public TarantoolClient tarantoolClient(
            @Value("${tarantool.nodes}") String nodes,
            @Value("${tarantool.username}") String username,
            @Value("${tarantool.password}") String password)
```


We will use `username` and `password` fields to create credentials for authentication:


``` lua
SimpleTarantoolCredentials credentials = new SimpleTarantoolCredentials(username, password);
```


Let's create a custom configuration for connecting to the cluster, namely specify the authentication parameters and the request timeout:


``` lua
TarantoolClientConfig config = new TarantoolClientConfig.Builder()
                .withCredentials(credentials)
                .withRequestTimeout(1000*60)
                .build();
```


Then we have to pass the list of nodes to the `AddressProvider` which converts a string into a list of addresses and returns this list:


``` lua
TarantoolClusterAddressProvider provider = new TarantoolClusterAddressProvider() {
            @Override
            public Collection<TarantoolServerAddress> getAddresses() {
                ArrayList<TarantoolServerAddress> addresses = new ArrayList<>();

                for (String node: nodes.split(",")) {
                    String[] address = node.split(":");
                    addresses.add(new TarantoolServerAddress(address[0], Integer.parseInt(address[1])));
                }

                return addresses;
            }
        };
```


Finally, let's create a client that will connect to the cluster. We wrap it into a proxy-client and return the result wrapped into a retrying-client, which, if the connection fails, tries to reconnect until it reaches the specified number of attempts:


``` lua
ClusterTarantoolTupleClient clusterClient = new ClusterTarantoolTupleClient(config, provider);
        ProxyTarantoolTupleClient proxyClient = new ProxyTarantoolTupleClient(clusterClient);

        return new RetryingTarantoolTupleClient(
                proxyClient,
                TarantoolRequestRetryPolicies.byNumberOfAttempts(
                        10, e -> e.getMessage().contains("Unsuccessful attempt")
                ).build());
```


Full code of the class:


``` lua
@Configuration
@PropertySource(value="classpath:application-tarantool.properties", encoding = "UTF-8")
public class TarantoolConfig {

    @Bean
    public TarantoolClient tarantoolClient(
            @Value("${tarantool.nodes}") String nodes,
            @Value("${tarantool.username}") String username,
            @Value("${tarantool.password}") String password) {

        SimpleTarantoolCredentials credentials = new SimpleTarantoolCredentials(username, password);

        TarantoolClientConfig config = new TarantoolClientConfig.Builder()
                .withCredentials(credentials)
                .withRequestTimeout(1000*60)
                .build();

        TarantoolClusterAddressProvider provider = new TarantoolClusterAddressProvider() {
            @Override
            public Collection<TarantoolServerAddress> getAddresses() {
                ArrayList<TarantoolServerAddress> addresses = new ArrayList<>();

                for (String node: nodes.split(",")) {
                    String[] address = node.split(":");
                    addresses.add(new TarantoolServerAddress(address[0], Integer.parseInt(address[1])));
                }

                return addresses;
            }
        };

        ClusterTarantoolTupleClient clusterClient = new ClusterTarantoolTupleClient(config, provider);
        ProxyTarantoolTupleClient proxyClient = new ProxyTarantoolTupleClient(clusterClient);

        return new RetryingTarantoolTupleClient(
                proxyClient,
                TarantoolRequestRetryPolicies.byNumberOfAttempts(
                        10, e -> e.getMessage().contains("Unsuccessful attempt")
                ).build());
    }
}
```


The application will connect to the cluster after the first request was sent to Tarantool on the application's launching. Now let's move on to creating an API and a user data model for our application.


### Creating an API and a user data model


We're going to use the OpenAPI specification of version 3.0.3. Let's create three endpoints, each of which will accept and process the corresponding types of requests:


-   `/register`
    -   POST, creating a user.
-   `/login`
    -   POST, user authentication.
-   `/{login}`
    -   GET, obtaining user information;
    -   PUT, updating user information;
    -   DELETE, deleting a user.


We will also add descriptions for the methods that handle each request we send and each response the application returns:


-   `authUserRequest`
-   `authUserResponse`
-   `createUserRequest`
-   `createUserResponse`
-   `getUserInfoResponse`
-   `updateUserRequest`


The stored procedures we've implemented in Lua will be called by controllers when processing these methods.

Now we need to generate classes that correspond to the described methods and responses. We'll use the swagger-codegen plugin for that. Add the plugin description to the `build` section of the *pom.xml* file:


``` lua
<plugin>
   <groupId>io.swagger.codegen.v3</groupId>
   <artifactId>swagger-codegen-maven-plugin</artifactId>
   <version>3.0.21</version>
   <executions>
      <execution>
         <id>api</id>
         <goals>
            <goal>generate</goal>
          </goals>
          <configuration>
             <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
             <language>java</language>
             <modelPackage>org.tarantool.models.rest</modelPackage>
             <output>${project.basedir}</output>
             <generateApis>false</generateApis>
             <generateSupportingFiles>false</generateSupportingFiles>
             <generateModelDocumentation>false</generateModelDocumentation>
             <generateModelTests>false</generateModelTests>
             <configOptions>
                <dateLibrary>java8</dateLibrary>
                <library>resttemplate</library>
                <useTags>true</useTags>
                <hideGenerationTimestamp>true</hideGenerationTimestamp>
             </configOptions>
         </configuration>
      </execution>
   </executions>
</plugin>
```


In these lines, we specify the path to the *api.yaml* file that describes the API, and the path to the directory where the generated Java files are to be placed. After running the build, we will get the generated request and response classes, which we are going to use when creating controllers.

Let's move on to creating a user data model. The corresponding class will be called `UserModel` and we'll place it in the *models* directory. In the same directory, in its *rest* subdirectory, there are also the classes for requests and responses. The model will describe the user and will contain three private fields: `uuid`, `login` and `password`. It will also have getters and setters to access these fields. So, our data model's class goes as follows:


``` lua
public class UserModel {

    String uuid;
    String login;
    String password;

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
```



### Creating services and controllers


In order to work with Tarantool when processing queries, we are going to use services that allow us to hide all the logic by calling methods of a certain class. We're going to use four basic methods:


-   `getUserByLogin` to get the user's information by their login;
-   `createUser` to create a new user;
-   `updateUser` to update the information of a user;
-   `deleteUser` to delete a user by their login.


To describe the basic service, let's create an interface that contains the signatures of these four methods, and then inherit the service that will contain our Tarantool logic from it. We'll call it `StorageService`:


``` lua
public interface StorageService {

    UserModel getUserByLogin(String login);

    String createUser(CreateUserRequest request);

    boolean updateUser(String login, UpdateUserRequest request);

    boolean deleteUser(String login);
}
```


Now, let's create the `TarantoolStorageService` class inherited from this interface. First, we must create a constructor for this class that will take `TarantoolClient` as input to be able to make queries to Tarantool. Let's save the client in a private variable and add the `final` modifier to it:


``` lua
private final TarantoolClient tarantoolClient;

    public TarantoolStorageService(TarantoolClient tarantoolClient) {
        this.tarantoolClient = tarantoolClient;
    }
```


Now let's override the method of getting the user by login. First, we create a variable `userTuple` of `List<ObjРµct>` type initialized by the `null` value:


``` lua
List<Object> userTuple = null;
```


After the initialization, we try to execute `tarantoolClient`'s method `call`, which will result in `Future`. Since this method is asynchronous, we call the `get` method with `0` argument to get the result of its execution. If an exception is thrown during the `call` method execution, we should catch it and log it to the console.


``` lua
try {
    userTuple = (List<Object>) tarantoolClient.call("get_user_by_login",login).get().get(0);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}
```


And if the method was executed successfully, we create an object of the `UserModel` class, fill all the fields and return it. Otherwise, we return `null`.


``` lua
if(userTuple != null) {
            UserModel user = new UserModel();
            user.setUuid((String)userTuple.get(1));
            user.setLogin((String)userTuple.get(2));
            user.setPassword((String)userTuple.get(3));

            return user;
        }

        return null;
```


Full code of the `getUserByLogin` method:


``` lua
public UserModel getUserByLogin(String login) {

        List<Object> userTuple = null;

        try {
            userTuple = (List<Object>) tarantoolClient.call("get_user_by_login", login).get().get(0);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        if(userTuple != null) {
            UserModel user = new UserModel();
            user.setUuid((String)userTuple.get(1));
            user.setLogin((String)userTuple.get(2));
            user.setPassword((String)userTuple.get(3));

            return user;
        }

        return null;
    }
```


We override other methods in the same way, but with some changes. Since the logic is quite similar to the one presented above, I'll just provide the full code of this class:


``` lua
@Service
public class TarantoolStorageService implements StorageService{

    private final TarantoolClient tarantoolClient;

    public TarantoolStorageService(TarantoolClient tarantoolClient) {
        this.tarantoolClient = tarantoolClient;
    }

    @Override
    public UserModel getUserByLogin(String login) {

        List<Object> userTuple = null;

        try {
            userTuple = (List<Object>) tarantoolClient.call("get_user_by_login", login).get().get(0);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        if(userTuple != null) {
            UserModel user = new UserModel();
            user.setUuid((String)userTuple.get(1));
            user.setLogin((String)userTuple.get(2));
            user.setPassword((String)userTuple.get(3));

            return user;
        }

        return null;
    }

    @Override
    public String createUser(CreateUserRequest request) {

        String uuid = UUID.randomUUID().toString();
        List<Object> userTuple = null;

        try {
            userTuple = (List<Object>) tarantoolClient.call("create_user",
                    uuid,
                    request.getLogin(),
                    DigestUtils.md5DigestAsHex(request.getPassword().getBytes())
            ).get();
        } catch(InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        if(userTuple != null) {
            return (String) userTuple.get(0);
        }

        return null;
    }

    @Override
    public boolean updateUser(String login, UpdateUserRequest request) {

        List<Object> userTuple = null;

        try {
            userTuple = (List<Object>) tarantoolClient.call("update_user_by_login",
                    login, DigestUtils.md5DigestAsHex(request.getPassword().getBytes())
            ).get().get(0);
        } catch(InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        return userTuple != null;
    }

    @Override
    public boolean deleteUser(String login) {
        List<Object> userTuple = null;

        try {
            userTuple = (List<Object>) tarantoolClient.call("delete_user_by_login",
                    login
            ).get().get(0);
        } catch(InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        return userTuple != null;
    }
}
```


After implementing this auxiliary service, we need to create services that contain user authentication and modification logic. The service for modifying and retrieving information about the user will be called `UserService`. It is quite straightforward in its implementation, as it's initialized by an object of the `StorageService` class and simply calls the methods defined in it. So I'll just provide the full code for this class, too:


``` lua
@Service
public class UserService {
    private final StorageService storageService;

    public UserService(StorageService storageService) {
        this.storageService = storageService;
    }

    public String createUser(CreateUserRequest request) {
        return this.storageService.createUser(request);
    }

    public boolean deleteUser(String login) {
        return this.storageService.deleteUser(login);
    }

    public UserModel getUserByLogin(String login) {
        return this.storageService.getUserByLogin(login);
    }

    public boolean updateUser(String login, UpdateUserRequest request) {
        return this.storageService.updateUser(login, request);
    }
}
```


The second service, which authenticates the user, we will call `AuthenticationService`. It will also be initialized with an object of the `StorageService` class and will contain only one method, `authenticate`, responsible for user authentication. How exactly is the authentication performed? This method calls the user's information from Tarantool by the user's login. Then it calculates the MD5 hash of the password and compares it with the one received from Tarantool. If the hashes match, the method returns a token, which for simplicity is just the user UUID, otherwise, it returns `null`. Full code of the `AuthenticationService` class:


``` lua
@Service
public class AuthenticationService {

    private final StorageService storageService;

    public AuthenticationService(StorageService storageService) {
        this.storageService = storageService;
    }

    public AuthUserResponse authenticate(String login, String password) {
        UserModel user = storageService.getUserByLogin(login);

        if(user == null) {
            return null;
        }

        String passHash = DigestUtils.md5DigestAsHex(password.getBytes());

        if (user.getPassword().equals(passHash)) {

            AuthUserResponse response = new AuthUserResponse();
            response.setAuthToken(user.getUuid());
            return response;

        } else {
            return null;
        }
    }
}
```


Now let's create two controllers responsible for authentication of the user and processing their information. The first one will be `AuthenticationController`, and the second one will be `UserController`.

Let's start with the `AuthenticationController`. Each controller is initialized with its own service, so we initialize the first one with an object of the `AuthenticationService` class. Our controller will also contain a mapping to the `/login` endpoint. It will parse the request, call the `authenticate` method of the service, and — based on the result of the call — return either UUID and code 200 or code 403 (Forbidden). Full code for this controller:


``` lua
@RestController
public class AuthenticationController {
    private final AuthenticationService authenticationService;

    public AuthenticationController(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    @PostMapping(value = "/login", produces={"application/json"})
    public ResponseEntity<AuthUserResponse> authenticate(@RequestBody AuthUserRequest request) {

        String login = request.getLogin();
        String password = request.getPassword();

        AuthUserResponse response = this.authenticationService.authenticate(login, password);

        if(response != null) {

            return ResponseEntity.status(HttpStatus.OK)
                    .cacheControl(CacheControl.noCache())
                    .body(response);
        } else {
            return new ResponseEntity<>(HttpStatus.FORBIDDEN);
        }
    }
}
```


The second controller, `UserController`, will be initialized with an object of the `UserService` class. It will contain mappings to the `/register` and `/{login}` endpoints. This controller's full code:


``` lua
@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping(value = "/register", produces={"application/json"})
    public ResponseEntity<CreateUserResponse> createUser(
            @RequestBody CreateUserRequest request) {
        String login = this.userService.createUser(request);

        if(login != null) {

            CreateUserResponse response = new CreateUserResponse();
            response.setLogin(login);

            return ResponseEntity.status(HttpStatus.OK)
                    .cacheControl(CacheControl.noCache())
                    .body(response);
        } else {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }
    }

    @GetMapping(value = "/{login}", produces={"application/json"})
    public ResponseEntity<GetUserInfoResponse> getUserInfo(
            @PathVariable("login") String login) {
        UserModel model = this.userService.getUserByLogin(login);
        if(model != null) {
            GetUserInfoResponse response = new GetUserInfoResponse();
            response.setUuid(model.getUuid());
            response.setLogin(model.getLogin());
            response.setPassword(model.getPassword());

            return ResponseEntity.status(HttpStatus.OK)
                    .cacheControl(CacheControl.noCache())
                    .body(response);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @PutMapping(value = "/{login}", produces={"application/json"})
    public ResponseEntity<Void> updateUser(
            @PathVariable("login") String login,
            @RequestBody UpdateUserRequest request) {
        boolean updated = this.userService.updateUser(login, request);

        if(updated) {
            return ResponseEntity.status(HttpStatus.OK)
                    .cacheControl(CacheControl.noCache())
                    .build();
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping(value = "/{login}", produces={"application/json"})
    public ResponseEntity<Void> deleteUser(
            @PathVariable("login") String login) {
        boolean deleted = this.userService.deleteUser(login);

        if(deleted) {
            return ResponseEntity.status(HttpStatus.OK)
                    .cacheControl(CacheControl.noCache())
                    .build();
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

}
```


This concludes the development of our Java application. All that's left to do now is build it. You can do that by running


``` lua
$ mvn clean package
```


After the application is built, you can run it with:


``` lua
$ java -jar ./target/authentication-example-1.0-SNAPSHOT.jar
```


Now we have finished developing our service! You can see its full code [**here**](https://github.com/droidroot1995/authentication-example).


### What was done



-   Installed the Java connector.
-   Set up a connection to the cluster.
-   Developed an API.
-   Created controllers and services.
-   Built our application.


What's left to do is to test the service.


## Checking if the service works


Let's check how correctly each of the requests is being processed. We'll use Postman for that task. We will use a test user with `login1` as their username and `password1` as their password.

We start by creating a new user. The request will look like this:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9j47ycrt3q750fpwyk7.png) 

The result is:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/op1hzl4d2egsw53wsrke.png)
 
 

Now let's check the authentication:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z59yjm6hma23nrzrqip0.png)
 
 

Check the user's data:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbyi5vhp462o8wr14hex.png)
 
 
 

Trying to update the user's password:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/baw2ge0zdczx9f85jiem.png)
 


Checking if the password was updated:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8s6w9lk2iq5fitd5cq3u.png)
 
 

Deleting the user:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y0as9ca816dwe58iy61z.png)
 

Trying to authenticate again:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qqeqs0l0pxfhotgksi9.png)
 
 

Checking the user's data again:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tps4nutmysukldjvm3g6.png)
 
 

All requests are executed correctly, we receive the expected results.


## Conclusion


As an example, we implemented an authentication system consisting of two applications:


1.  A Tarantool Cartridge application that implements the business logic for handling user information and data storage.
2.  A Java application providing an API for authentication.


Tarantool Cartridge is a framework for scaling and managing a cluster of multiple Tarantool instances, and also for developing cluster applications.

We used the Cartridge Java Connector, which replaced the outdated Tarantool Java Connector, to communicate between the applications we wrote. It allows you to work not only with single instances of Tarantool, but also with entire clusters, which makes the connector more versatile and irreplaceable for developing enterprise applications.


## Links


-    [Our official website](http://www.tarantool.io/?utm_source=dev.to&utm_medium=referral&utm_campaign=2022)
-    [Ask your questions to the community in the Telegram chat](http://t.me/tarantool?utm_source=dev.to&utm_medium=referral&utm_campaign=2022)
-   [Source code of the sample application on GitHub](https://github.com/droidroot1995/authentication-example)
-   [Tarantool Cartridge framework on GitHub](https://github.com/tarantool/cartridge)
-   [Cartridge Java connector on GitHub](https://github.com/tarantool/cartridge-java)
-   [Migrations module on GitHub](https://github.com/tarantool/migrations)