[JavaScript, Node.JS] Авторизация и аутентификация на NodeJs и Socket.io и проблемы вокруг

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
19-Янв-2021 15:30

На текущий момент я работаю в компании «МегаФон» тимлидом фронта. С начала 2020 года мы в команде МегаФона разрабатываем собственную платформу Интернета вещей. Так как в таком процессе нагрузка на бэк-энд разработчиков стала колоссальной, а фронт не так активно задействован, внутри отдела было принято решение отдать всю веб-часть в руки моей команды. Очевидно, что мы взяли NodeJs с ExpressJS, и занялись построением серверной архитектуры.Для корректного доступа к собранным с устройств данным нужна была авторизация, чтобы понимать, кто и что может получить/сделать. Про очевидный путь с passportJs, думаю, нет смысла рассказывать. По логину и паролю мы стали отдавать jwt токен, и изначально на этом успокоились.После этого нам потребовалось хранить в сессии данные, специфичные для каждого пользователя. Логичным решением было бы использовать сам jwt токен, хранить информацию в нем и гонять от клиента к серверу. Однако, данное решение не подходило нам из-за использования веб-сокетов (в нашем случае мы взяли socket.io), так как в данном протоколе передача хедера Authorization с jwt токеном невозможна (в соответствии со стандартом). Единственный вариант - передавать хедер в параметрах url. Но это не очень здорово - токены будут легко видны во всех логах всех прокси-серверов. Хорошим решением оказалось использование сессии, которая хранится полностью на серверной стороне, и по сети ходит лишь id этой сессии. Мы выбрали - express-session. Объединенная сессияОтдельной проблемой стала необходимость получения актуального состояния сессии и возможность его изменения в событиях веб-сокетов. Для этого идеально подошел пакет - express-socket.io-session. Правда, пришлось поколдовать над её подключением:Изменили подключение сессии и настройки кук:
this.store = new pgSession({
          pool: pgPool,
          tableName: SESSION_TABLE
      });
this.session = expressSession({
    name: SESSION_KEY,
    secret: SESSION.secret,
    resave: false, // важно, для того, чтобы сессия не перезаписывалась на каждый чих
    rolling: true,
    saveUninitialized: true, // нужно для выдачи куки даже неавторизированному пользователю
    proxy: true,
    cookie: {
        secure: true, // обязывает производить передачу по ssl
        maxAge: SESSION_DURATION,
        sameSite: 'none' // чтобы можно было отдавать на разные поддомены
    }
    store: this.store
});
Мы написали обработчики сессии таким образом, чтобы сессия подгружалась до начала обработки события, и сохранялась, если необходимо:
const asyncHandlerExtended = (fn, socket) => (data) => {
    const cb = async () => {
        await reloadSession(socket.handshake.session);
        await fn({ socket, data });
        await saveSession(socket.handshake.session);
    };
    return Promise.resolve(cb()).catch((err) => {
        socket.emit('error', err);
    });
};
Собрали все вместе при настройке сокетов:
import sharedSession from 'express-socket.io-session';
import io from 'socket.io';
const resultSocket = nameSpace ? this.io.of(nameSpace) : this.io;
resultSocket.use(sharedSession(session, { autoSave: true }));
Разделение сокетов по ролямДальше нам нужно понимание того, кому и какие события можно получать на сокетах, а какие - нет. Для этого отлично подходит механизм комнат в socket.io. Он позволяет серверу формировать пространства, в которые можно "запускать" пользователей и эмитить в них разные события. Мы выделили под каждую из ролей пользователей отдельную комнату (например комната adminRoom - пространство для событий, которые могут идти/поступать только для администраторов), а общее пространство теперь у нас считается "публичным" и доступно для всех подключенных, но не авторизованных пользователей. Таким образом, процесс получения доступов на сокетах выглядит так:
  • Клиент аутентифицируется по http, по паре логин/пароль, получает в ответ jwt токен и куку с id сессии.
  • Далее юзер коннектится к нашей точке входа для socket.io (например: localhost:8080/sockets). Теперь у него есть доступ до публичных событий на наших сокетах.
  • Если он хочет получить доступ до всех наших событий, которые ему доступны по роли, то он отправляет событие auth_login по сокетам, с jwt токеном, который он получил от http авторизации.
  • Система проверяет токен и по результатам проверки генерирует одно из двух событий.
    • auth_loginFailed - пользователю не будут предоставлены доступы, так как токен кривой или просрочен
    • auth_loginSuccess - все хорошо, можно продолжать
  • Если проверка прошла успешно, то сервер добавляет пользователя во все пространства предоставленные ему по его роли.
  • Пользователю теперь доступны аутентифицированные и скрытые ранее события.
  • Когда у токена проходит "срок годности", сокеты генерируют событие auth_expire, говорящее, что более пользователю недоступны ранее предоставленные комнаты.
Token stealВишенкой на торте в данной картине механизма авторизации/аутентификации стал результат изучения проблемы кражи токенов. Ради минимизации рисков от попадания в такую ситуацию, было решено улучшить механизмы работы с авторизационным токеном. Во время исследования данной темы наткнулся на статью на хабре - Зачем нужен Refresh Token, если есть Access Token?. Очень советую ознакомиться, но если кратко, то вот результирующая цитата:
Таким образом, схема refresh + access токен ограничивает время, на которое атакующий может получить доступ к сервису. По сравнению с одним токеном, которым злоумышленник может пользоваться неделями и никто об этом не узнает.
Однако, у нас уже есть два токена:
  • id серверной сессии от express-session, который ходит в куках, всегда
  • jwt токен, который генерируется после логина пользователя
Поэтому было бы логично реализовать похожий механизм, как и в OAuth2. Для этого мы стали хранить jwt токен в сессии и сравнивать его с полученным от клиента при проверке аутентификации. Остается только одна проблема - несколько токенов в одной сессии. Необходимо это для того, чтобы иметь возможность войти и в админку, и на фронт. Для этого считаем, что клиент передаст свое "название" при логине, и под этим названием мы и сохраним токен в сессию, а также записываем само название внутрь токена для дальнейшей проверки. За счет вышеописанного получается следующая схема взаимодействия:
  • Клиент отправляет пару «логин и пароль», плюс к этому уникальный ключ - название себя (то есть имя приложения).
  • Система, если пара «логин и пароль» найдена, генерирует jwt токен, включая в него название клиента (ключ), отправляет его клиенту и записывает в сессию.
  • При последующем обращении клиента по роуту, который закрыт проверкой аутентификации, проверяется наличие токена, его валидность, просрочен ли он. Далее сервер вытаскивает из токена название клиента и смотрит, есть ли такой токен с таким ключом в текущей сессии.
  • Если вдруг токен и куку украл злоумышленник, то при первом же рефреше с любой из сторон (клиент или злоумышленник), сторона, которая попытается обратиться со старой парой «токен + кука», получит негативный результат. Система поймет, что токен не соответствует тому, что хранит сессия и очистит сессию полностью, что привет к выбросу и клиента и злоумышленника, а факт кражи будет зафиксирован в системе.
  • Если же все хорошо, то само собой мы отдадим данные =)
Таким образом, мы получили многосессионный механизм (один пользователь может зайти сразу с нескольких браузеров в одно приложение) аутентификации, который работает еще и несколько приложений сразу.В заключениеНадеюсь, описанная выше логика работы поможет читателям реализовать достаточно защищенные веб-сервера. На текущий момент в статье приведено мало снипетов кода, так как логика разбросана по разным файлам и частям кода, и собрать ее воедино будет проблематично. Но если будет проявлен интерес в комментариях к решению той или иной проблемы, я постараюсь дополнить статью тем или иным куском кода, примером и т.п.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_node.js, #_avtorizatsija (авторизация), #_socket.io, #_avtorizatsija_polzovatelja (авторизация пользователя), #_javascript, #_node.js
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 16:29
Часовой пояс: UTC + 5