Applications for Tarantool 1.7. Part 2: OAuth2 authorization via Facebook

Original article available at https://habrahabr.ru/company/mailru/blog/336648/

How do you build your own Tarantool application without overcomplicating things unnecessarily? This second tutorial in the three-part Tarantool series will cover networking as well as installing and using third-party Tarantool packages.

If you’ve missed part 1, you can find it here.

Interaction with external services

As an example, let’s take a look at how OAuth2 authorization via Facebook is implemented in the tarantool-authman application. With OAuth2 authorization, a user clicks a link that takes them to the Facebook login page. After the user enters their authorization credentials and grants necessary permissions, the social network redirects them back to the site with an authorization code embedded into the redirection URI as a GET parameter. The server then exchanges this code for a token (or a pair of tokens — access and refresh): it allows obtaining information about this user directly from Facebook. To learn more about how OAuth2 authorization works, check this article.

The Tarantool application handles the code-for-token exchange and uses the token to get information about the user from the social network; in our case, the user’s email and first and last name. To exchange an authorization code for a token, it’s necessary to send Facebook an HTTP request containing the code itself, alongside the application’s Facebook parameters, client_id and client_secret.

Tarantool 1.7.4–151 has a built-in http.client package that is based on libcurl. This package allows receiving and sending HTTP requests, so let’s use it to implement OAuth2 authorization. We’ll begin by creating a helper function for sending HTTP requests in the authman/utils/http.lua package:

local http = {}
local utils = require(‘authman.utils.utils’)
local curl_http = require(‘http.client’)
-- config — general configuration for the authman application
function http.api(config)
local api = {}
-- Configuring network requests
local timeout = config.request_timeout
    function api.request(method, url, params, param_values)
local response, body, ok, msg
        if method == ‘POST’ then
-- utils.format — helper function for placeholder
-- substitution

body = utils.format(params, param_values)
            -- Safe call to pcall won’t interrupt the program
-- execution in case of a network error

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

In-app OAuth2 authorization

Let’s create a social model and implement a get_token(provider, code) method for obtaining a token by an authorization code, and a get_profile_info(provider, token, user_tuple) method for getting and updating information about a given user. Below is the corresponding code:

-- Method for obtaining a token
function model.get_token(provider, code)
local response, data, token
if provider == ‘facebook’ then
-- In this context, http is the authman/utils/http.lua
-- package

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 — project configuration passed to the
-- model upon initialization

-- Application’s parameters in the social network
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
-- Method for getting a user profile
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, let’s design an API method for creating a new user or for logging in via Facebook with an existing one. This method should return a user and session data. To learn more about how sessions are created and validated, look at the source code here.

-- API method in 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
    -- Getting an OAuth2 token
token = social.get_token(provider, code, user_tuple)
if not validator.not_empty_string(token) then
return response.error(error.WRONG_AUTH_CODE)
end
    -- Getting information about a user
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
    -- Checking if our space already has a user with this social_id
social_tuple = social.get_by_social_id(social_id, provider)
if social_tuple == nil then
-- If not, creating a new user
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
-- If it does, updating a user profile
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
    -- Creating a user session
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 do we check if this method really works? For starters, it’s necessary to register the application on the Facebook for Developers portal. In our application, we need to add a Facebook Login product and, in the Valid OAuth redirect URIs field, specify redirect_uri, which is a URL of your site where the social network redirects users with an authorization code once they’re successfully authorized in the social network. After that, in a web browser, open the following URL: https://www.facebook.com/v2.8/dialog/oauth?client_id=${client_id}&redirect_uri=${redirect_uri}&scope=email, where:

  • client_id is your application’s Facebook ID;
  • redirect_uri is the redirection URL you specified earlier;
  • scope is a list of permissions (in our case, it’s only email).

Facebook will then ask you to confirm you’re granting it the listed permissions and, following your confirmation, redirect you with an authorization code embedded into the redirection URL as a GET parameter. This is the very same authorization code that is passed to the api.social_auth() method. Before we check if our code works as expected, let’s create authman/config/config.lua, a configuration file holding the application’s Facebook parameters.

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

Now, we can make sure our code works and the application obtains a user profile 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’: ‘Ivan’, ‘last_name’: ‘Ivanov’}
id: b1e1fe02–47a2–41c6-ac8e-44dae71cde5e
email: ivanov@mail.ru
session: …

Installing additional packages

It’s always nice to have readily available solutions when dealing with various problems. For example, in Tarantool versions older than 1.7.4–151, it was impossible to send an HTTP request out of the box — you needed a tarantool-curl package for it. But it is deprecated now and therefore not recommended for use. There are many other useful packages out there, though, and one of them is tarantool-queue that implements a FIFO queue.

There exist several ways to install this package, but the simplest one has appeared only recently, in Tarantool 1.7.4–294:

$ tarantoolctl rocks install queue

Other Tarantool packages can also be installed via a package manager. The complete list of Tarantool packages is available on the Rocks page.

Another way of installing tarantool-queue is via a package manager that comes with your OS. You need to add the Tarantool repository to the list of available repositories, if you haven’t done so already during the installation, and make sure that the package you need is indeed in the repository. For example, if you’re on Ubuntu, say this:

$ sudo apt-get install tarantool-queue

A third installation method is somewhat more complicated, but it allows using not only Tarantool applications, but also Lua packages, both of which are easy to install with a LuaRocks package manager. You can refer to the documentation to learn more about LuaRocks and available packages. Let’s install LuaRocks and configure it to work with the Tarantool repository:

$ sudo apt-get install luarocks

Now, we need to configure LuaRocks so that we can install not only Lua packages, but Tarantool packages as well. To do that, we create a ~/.luarocks/config.lua file with the following settings:

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

Finally, we can install tarantool-queue and check if it works:

# Installing tarantool-queue
$ sudo luarocks install queue
# Launching the interactive console and checking if the package works:
$ 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’re able to create complex applications that interact with external services. In the next tutorial, we’ll talk about testing your applications, properly configuring them and deploying them to production. Thanks for reading and stay tuned!