Applications for Tarantool. Part 2. OAuth2 Authorization

  • Tutorial

How to build your application for Tarantool and at the same time not to fence the garden every time when you want to do a seemingly elementary thing? This is the continuation of a series of articles on how to create your own Tarantool applications.


Today we will consider issues of network interaction, installation and use of third-party modules.



Contents of the Tarantool Applications Series


- Part 1: Stored procedures
- Part 2: OAuth2 authorization
- Part 3: Testing and launching


Interaction with external services


As an example, consider the implementation of OAuth2 authorization via Facebook in the tarantool-authman application . During OAuth2 authorization, the user follows the link that leads to the login window on the social network. After entering the authorization data and confirming permissions (permisssions), the social network redirects the user back to the site with the authorization code in the GET request parameter. The server should exchange this code for a token (or for a pair of tokens - access and refresh). With a token, you can get information about the user from a social network. Read more about OAuth2 authorization in this article .



The Tarantool application will take over the exchange of the authorization code (code) for the access token (token) to user information from the social network, and will also receive user data for this token. In our case, this is email, name and surname. To exchange the authorization code for an access token, you need to send a request to Facebook with the code, as well as the parameters of the Facebook application - client_id and client_secret.


Tarantool from version 1.7.4-151 has a built-in module http.clientbased on it libcurl. The module allows you to receive and send HTTP requests. We will use this module to implement OAuth2 authorization. First, create an auxiliary function for sending HTTP requests in the authman / utils / http.lua module:


local http = {}
local utils = require('authman.utils.utils')
local curl_http = require('http.client')
-- config — общая конфигурация приложения authman
function http.api(config)
    local api = {}
    -- Конфигурация сетевых запросов
    local timeout = config.request_timeout
    function api.request(method, url, params, param_values)
        local response, body, ok, msg
        if method == 'POST' then
            -- utils.format — функция для подстановки значений в placeholder’ы
            body = utils.format(params, param_values)
            -- Безопасный вызов pcall не прервет исполнение программы при ошибке сети
            ok, msg = pcall(function()
                response = curl_http.post(url, body, {
                        headers = {['Content-Type'] = 'application/x-www-form-urlencoded'},
                        timeout = timeout
                })
            end)
        end
        return response
    end
    return api
end
return http

It is worth paying attention to the pcall function. It handles exceptions that occur during the execution of an anonymous function. In our case, it is necessary to handle the network errors that the HTTP client generates. The result of calling pcall is written to the variables ok (true / false) and msg (error message, nil on success).


OAuth2 application authorization


We will create a social model and write a method for obtaining a token from the authorization code get_token(provider, code), as well as a method for obtaining or updating profile data get_profile_info(provider, token, user_tuple). Consider these methods:


-- Метод получения токена
function model.get_token(provider, code)
    local response, data, token
    if provider == 'facebook' then
        -- Здесь http — модуль authman/utils/http.lua
        response = http.request(
            'GET',
            'https://graph.facebook.com/v2.8/oauth/access_token',
            '?client_id=${client_id}&redirect_uri=${redirect_uri}&client_secret=${client_secret}&code=${code}',
            {
                -- config — конфигурации проекта, которые передаются в модель при инициализации
                -- Это параметры приложения в социальной сети
                client_id = config[provider].client_id,
                redirect_uri = config[provider].redirect_uri,
                client_secret = config[provider].client_secret,
                code = code,
            }
        )
        if response == nil or response.code ~= 200 then
            return nil
        else
            data = json.decode(response.body)
            return data.access_token
        end
    end
end
-- Метод получения профиля пользователя
function model.get_profile_info(provider, token, user_tuple)
    local response, data
    user_tuple[user.PROFILE] = {}
    if provider == 'facebook' then
        response = http.request(
            'GET',
            'https://graph.facebook.com/me',
            '?access_token=${token}&fields=email,first_name,last_name',
            { token = token }
        )
        if response == nil or response.code ~= 200 then
            return nil
        else
            data = json.decode(response.body)
            user_tuple[user.EMAIL] = data.email
            user_tuple[user.PROFILE][user.PROFILE_FIRST_NAME] = data.first_name
            user_tuple[user.PROFILE][user.PROFILE_LAST_NAME] = data.last_name
            return data.id
        end
    end
end

Now we’ll add an application API method that allows you to create a user or log in with an existing one through Facebook. The method will return the user along with session data. More information about how a session is formed and validated can be found in the source code .


-- Метод api в authman/init.lua
function api.social_auth(provider, code)
    local token, social_id, social_tuple
    local user_tuple = {}
    if not (validator.provider(provider) and validator.not_empty_string(code)) then
        return response.error(error.WRONG_PROVIDER)
    end
    -- Получим OAuth2-токен
    token = social.get_token(provider, code, user_tuple)
    if not validator.not_empty_string(token) then
        return response.error(error.WRONG_AUTH_CODE)
    end
    -- Получим информацию о пользователе
    social_id = social.get_profile_info(provider, token, user_tuple)
    if not validator.not_empty_string(social_id) then
        return response.error(error.SOCIAL_AUTH_ERROR)
    end
    user_tuple[user.EMAIL] = utils.lower(user_tuple[user.EMAIL])
    user_tuple[user.IS_ACTIVE] = true
    user_tuple[user.TYPE] = user.SOCIAL_TYPE
    -- Проверим, есть ли в space пользователь с таким же social_id
    social_tuple = social.get_by_social_id(social_id, provider)
    if social_tuple == nil then
        -- Если нет — создадим его
        user_tuple = user.create(user_tuple)
        social_tuple = social.create({
            [social.USER_ID] = user_tuple[user.ID],
            [social.PROVIDER] = provider,
            [social.SOCIAL_ID] = social_id,
            [social.TOKEN] = token
        })
    else
        -- А если есть — обновим информацию профиля
        user_tuple[user.ID] = social_tuple[social.USER_ID]
        user_tuple = user.create_or_update(user_tuple)
        social_tuple = social.update({
            [social.ID] = social_tuple[social.ID],
            [social.USER_ID] = user_tuple[user.ID],
            [social.TOKEN] = token
        })
    end
    -- Создание пользовательской сессии
    local new_session = session.create(
        user_tuple[user.ID], session.SOCIAL_SESSION_TYPE, social_tuple[social.ID]
    )
    return response.ok(user.serialize(user_tuple, {
        session = new_session,
        social = social.serialize(social_tuple),
    }))
end

How to verify that the method works? First you need to register the application on Facebook. Let's do it on the Facebook developer page . In the created application, you need to add the product “Login via Facebook” and specify redirect_uri - “Valid URLs for OAuth redirection”. The redirect_uri parameter is the URL of your site where the social network will redirect the user with the code parameter after successful authorization in the social network. Next, open the url in your browser https://www.facebook.com/v2.8/dialog/oauth?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=email, where


• client_id - id of your application on Facebook;
• redirect_uri - URL for the redirect that you specified earlier;
• scope - a list of permissions (in this case, only email).


Facebook will ask for confirmation of permissions, after confirmation it will redirect you with the GET parameter code. This is the very authorization code that the method accepts api.social_auth(). Before checking the functionality of the code, create the configuration file authman / config / config.lua, in which we indicate the settings of the Facebook application.


return {
    facebook = {
        client_id = 'id from fb application',
        client_secret = 'secret from fb application'',
        redirect_uri='http://redirect_to_your_service',
    }
}

Now let's verify that the code works and the application receives information about the user from the social network:


$ tarantool
version 1.7.4-384-g70898fd
type 'help' for interactive help
tarantool> config = require('config.config')
tarantool> box.cfg({listen = 3331})
tarantool> auth = require('authman').api(config)
tarantool> code = 'auth_code_from_get_param'
tarantool> ok, user = auth.social_auth('facebook', code)
tarantool> user
---
- is_active: true
  social:
    provider: facebook
    social_id: '000000000000001'
  profile: {'first_name': 'Иван', 'last_name': 'Иванов'}
  id: b1e1fe02-47a2-41c6-ac8e-44dae71cde5e
  email: ivanov@mail.ru
  session: ...
...

Installing third-party modules


To solve many problems it’s good to have ready-made solutions at hand. For example, in versions Tarantool earlier than 1.7.4-151, sending an HTTP request out of the box was not possible. The tarantool-curl module was required . Now this module is no longer supported, it is not recommended to use it. However, there are many other useful modules, one of them - tarantool-queue - the implementation of the FIFO queue.


There are several ways to install tarantool-queue. The first, the simplest and most convenient, appeared relatively recently, in the Tarantool version 1.7.4-294.


$ tarantoolctl rocks install queue

Other Tarantool packages are also available for installation using the package manager. A complete list of modules for Tarantool can be found on the Rocks page .


The second way is using the package manager of your OS. Here you need to connect the Tarantool repository, if you have not connected it at the installation stage , and also make sure that the corresponding package is in the repository. For example, for Ubuntu:


$ sudo apt-get install tarantool-queue

The third method is more complicated, but it allows you to use not only applications for Tarantool, but also ready-made Lua modules. Installing modules for Tarantool and Lua is convenient using the LuaRocks package manager. Details about it and the available modules can be found in the documentation . Install LuaRocks and configure it to work with the Tarantool repository:


$ sudo apt-get install luarocks

Now configure LuaRocks to install not only Lua packages, but also packages for Tarantool. To do this, create a ~ / .luarocks / config.lua file with the following contents:


rocks_servers = {
    [[http://luarocks.org/repositories/rocks]],
    [[http://rocks.tarantool.org/]]
}

Install the module itself and check its operation:


# Установка tarantool-queue
$ sudo luarocks install queue
# Запустим интерактивную консоль и проверим работоспособность модуля:
$ tarantool
version 1.7.3-433-gef900f2
type 'help' for interactive help
tarantool> box.cfg({listen = 3331})
tarantool> queue = require('queue')
tarantool> test_queue = queue.create_tube('test_queue', 'fifo')
tarantool> test_queue:put({'task_1'})
tarantool> test_queue:put({'task_2'})
tarantool> test_queue:take()
---
- [0, 't', ['task_1']]
...

So, now we can create applications with complex architecture and external interactions. In the next part, we will examine application testing, as well as configuration and launch in battle. See you soon!


Also popular now: