Есть ли корпоративная жизнь на удаленке и как ее обеспечить: интеграция внутренней системы аутентификации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В digital-агентстве Convergent, где я работаю, в потоке множество проектов, и у каждого из них может быть собственная админка. Есть несколько окружений (дев, стейдж, лайв). А ещё есть разные внутрикорпоративные сервисы (как собственной разработки, так и сторонние вроде Redmine или Mattermost), которыми ежедневно пользуются сотрудники. 

Наша команда всегда была распределённой между несколькими офисами, но с учётом событий последнего года все сотрудники перешли на удалёнку. Так мы столкнулись с необходимостью организовать всё многообразие внутренних и клиентских сервисов в единой системе.

В данной статье я хочу поделиться опытом создания собственной внутренней системы аутентификации на основе OpenResty, а также спецификации OAuth2. В качестве основного языка программирования мы используем PHP, а фреймворк ― Yii 2.

Суммирую необходимый функционал:

  • Единое место управления всеми доступами. Здесь происходит выдача и отзыв доступов в административные панели сайтов и списки доменов;

  • По умолчанию весь доступ запрещён, если не указано обратное (доступ к любым доменам контролируется с помощью OpenResty);

  • Аутентификация для сотрудников;

  • Аутентификация для клиентов.

Закрытый доступ к сайтам и инфраструктуре

Начну со схемы, как мы организовали инфраструктуру доступов.

Упрощенная схема взаимодействия между пользователями и серверами
Упрощенная схема взаимодействия между пользователями и серверами

Первое, что нужно было сделать, ― это закрыть доступ по умолчанию ко всем тестовым окружениям и инфраструктурным сервисам. Сотрудники в таком случае могут получить доступ ко всему путём добавления своего IP в вайтлист (об этом позже), а клиенты получают доступ точечно.

Фронт-контроллером в данном случае выступает OpenResty ― это модифицированная версия nginx, которая в т. ч. поддерживает из коробки язык Lua. На нём я написал прослойку, через которую проходят все HTTP(s)-запросы.

Вот так может выглядеть код скрипта аунтентификации (в упрощенном варианте):

auth.lua
function authenticationPrompt()
    ngx.header.www_authenticate = 'Basic realm="Restricted by OpenResty"'
    ngx.exit(401)
end

function hasAccessByIp()
    local ip = ngx.var.remote_addr
    local domain = ngx.var.host
    local port = ngx.var.server_port

    local res, err = httpc:request_uri(os.getenv("AUTH_API_URL") .. "/ip.php", {
        method = "GET",
        query = "ip=" .. ip .. '&domain=' .. domain .. '&port=' .. port,
        headers = {
          ["Content-Type"] = "application/x-www-form-urlencoded",
        },
        keepalive_timeout = 60000,
        keepalive_pool = 10,
        ssl_verify = false
    })

    if res ~= nil then
        if (res.status == 200) then
            session.data.domains[domain] = true
            session:save()

            return true
        elseif (res.status == 403) then
            return false
        else
            session:close()
            ngx.say("Server error: " .. res.body)
            ngx.exit(500)
        end
    else
        session:close()
        ngx.say("Server error: " .. err)
        ngx.exit(500)
    end
end

function hasAccessByLogin()
    local header = ngx.var.http_authorization
    local domain = ngx.var.host
    local port = ngx.var.server_port

    if (header ~= nil) then
        header = ngx.decode_base64(header:sub(header:find(' ') + 1))
        login, password = header:match("([^,]+):([^,]+)")

        if login == nil then
            login = ""
        end
        if password == nil then
            password = ""
        end

        local res, err = httpc:request_uri(os.getenv("AUTH_API_URL") .. '/login.php', {
            method = "POST",
            body = "username=" .. login .. '&password=' .. password .. '&domain=' .. domain .. '&port=' .. port,
            headers = {
              ["Content-Type"] = "application/x-www-form-urlencoded",
            },
            keepalive_timeout = 60000,
            keepalive_pool = 10,
            ssl_verify = false
        })

        if res ~= nil then
            if (res.status == 200) then
                session.data.domains[domain] = true
                session:save()

                return true
            elseif (res.status == 403) then
                return false
            else
                session:close()
                ngx.say("Server error: " .. res.body)
                ngx.exit(500)
            end
        else
            session:close()
            ngx.say("Server error: " .. err)
            ngx.exit(500)
        end
    else
        return false
    end
end

os = require("os")

http = require "resty.http"
httpc = http.new()

session = require "resty.session".new()
session:start()

if (session.data.domains == nil) then
    session.data.domains = {}
end

local domain = ngx.var.host

if session.data.domains[domain] == nil then
    if (not hasAccessByIp() and not hasAccessByLogin()) then
        session:close()
        authenticationPrompt()
    else
        session:close()
    end
else
    session:close()
end

Алгоритм работы скрипта довольно простой:

  • Поступает HTTP-запрос от пользователя;

  • OpenResty запускает скрипт auth.lua;

  • Скрипт определяет запрашиваемый домен и отправляет два запроса на внешний бэкенд;

  • Первый ― на проверку IP-адреса пользователя в базу;

  • Если IP отсутствует, выводит браузерное окно для ввода логина и пароля, отправляет второй запрос на проверку доступа;

  • В любой другой ситуации выводит окно Вход.

На GitHub я выложил рабочий пример, который можно быстро развернуть с помощью Docker.

Немного расскажу о том, как выглядит управление в нашей системе.

Отмечу, что IP-адреса попадают в базу при аутентификации пользователя. Это сделано специально для сотрудников. Такие адреса помечают как временные и доступные ограниченное время, после чего они удаляются.

В случаях, когда нужно разрешить межсерверное взаимодействие, через панель управления можно добавить IP-адреса вручную.

Для клиентов же создаются простые доступы по паре логин-пароль. Такие доступы ограничиваются в пределах определенных доменных адресов.

Управление доступами по паролю и по IP-адресу
Управление доступами по паролю и по IP-адресу

Управление базой сотрудников подтягивается в ID через синхронизацию с другой внутренней системой. Каждый новый сотрудник при добавлении в нашу CRM автоматически получает учётную запись в ID, а также все нужные письма с доступами и инструкциями для последующей настройки и работы.

Форма аутентификации для сотрудников
Форма аутентификации для сотрудников

Каждый день мы начинаем работу с данной аутентификации, таким образом добавляя свой IP-адрес в белый список для получения доступа к инфраструктуре.

Двухфакторная аутентификация

Для обеспечения двухфакторной аутентификации для сотрудников решено было добавить Google Authenticator. Такой механизм защиты позволяет больше обезопасить себя от утечки доступов. Для PHP есть готовая библиотека sonata-project/GoogleAuthenticator. Пример интеграции можно посмотреть здесь.

Интересный нюанс, с которым мы столкнулись в процессе, это зависимость генерируемого кода от времени на устройстве пользователя. Выяснилось, что у некоторых сотрудников время на смартфоне немного отличалось от реального.

OAuth и OpenID

Третье, и не менее важное для нас, ― создание OAuth-сервера. За основу мы взяли модуль thephpleague/oauth2-server. Для Yii 2 готового решения не было, поэтому написали собственную имплементацию сервера. OAuth2 ― достаточно обширная тема, расписывать её работу в данной статье не буду. Библиотека имеет хорошую документацию. Также она поддерживает различные фреймворки, включая Laravel и Symfony.

Таким образом, любой сторонний сервис, который поддерживает кастомные OAuth2 конфигурации, достаточно просто подключается к нашей системе. Значимой “фишкой” такой интеграции стало подключение нашего ID к Mattermost. Последний в бесплатной версии поддерживает только аутентификацию с помощью GitLab, которую удалось эмулировать через наш сервис.

Также для всех наших проектов на Yii был разработан модуль для подключения ID. Это позволило вынести всё управление доступами в админпанели для сотрудников в централизованное место. Кстати, если интересно, я писал статью о модульном подходе, который мы применили в нашем digital-агентстве.

Заключение

Процесс адаптации компании под новую систему был относительно сложный, т. к. это потребовало доработки инфраструктуры и обучения работе с системой всех сотрудников компании. Просто так сказать “вот у нас теперь ID, пользуйтесь им” не получится, конечно, поэтому весь процесс миграции был очень тщательно задокументирован, а я выступал в качестве технической поддержки первые пару месяцев. Сейчас, спустя время, все процессы наладились, были внесены корректировки и добавлены новые фичи (например, автоматические письма новым сотрудникам с доступами и инструкциями).

Самое главное ― получилось создать для сотрудников единую учётную запись для всех внутренних и внешних систем, которыми пользуемся в работе, а также автоматизировать процесс получения этой учётной записи и всей нужной информации сразу же в первый день работы новых сотрудников.

Ссылки по теме

  • Мой пример Basic Digest аутентификации на Lua и OpenResty

  • GoogleAuthenticator на PHP

  • OAuth2 сервер на PHP

Источник: https://habr.com/ru/post/555154/


Интересные статьи

Интересные статьи

Несмотря на то, что решения для удаленного доступа уже много лет успешно работают на рынке и имеют многомиллионную аудиторию, они всегда вызывали у пользователей некоторые опасения в...
Привет! Меня зовут Леша Свиридо, я ведущий дизайнер продукта в Альфа-Банке (это мы делаем интернет-банк для бизнеса). В этом посте я поведаю про дизайн-системы. Да, про них пишут так же час...
Когда: 12 февраля 2020 г. с 19:00 до 20:30 по московскому времени. Кому будет полезно: ИТ-менеджерам и юристам иностранных компаний, начинающих или планирующих работать в России. О чем ...
Есть статьи о недостатках Битрикса, которые написаны программистами. Недостатки, описанные в них рядовому пользователю безразличны, ведь он не собирается ничего программировать.
Найти кусочек земной поверхности, не попадающий в поле зрения какой-либо камеры, становится всё сложнее, если говорить о более-менее крупных городах. Кажется, ещё немного, и настанет то самое...