[JavaScript, Node.JS] Авторизация и аутентификация на NodeJs и Socket.io и проблемы вокруг
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
На текущий момент я работаю в компании «МегаФон» тимлидом фронта. С начала 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, VueJS] Впечатления о Vue.js после React
- [JavaScript, ReactJS] Первое погружение в исходники хуков (задел на будущие статьи)
- [JavaScript, Разработка игр, Usability, Тестирование игр] Управляемость транспортного средства в симуляторе: настраиваем коэффициенты модели
- [JavaScript, TypeScript] Ant Design Component Customization and Bundle Optimization
- [Разработка веб-сайтов, JavaScript, Node.JS, ReactJS, Поисковая оптимизация] Server-Side Rendering с нуля до профи
- [JavaScript, API] Конструктор плейлистов для Spotify
- [JavaScript] Приглашаем на DINS JS EVENING: разбираем Chrome DevTools и Cypress
- [Разработка веб-сайтов, JavaScript, Интерфейсы, Big Data, TypeScript] Автоматическая виртуализация рендеринга произвольной вёрстки
- [Информационная безопасность, Open source, JavaScript, Node.JS] CRUD для NMAP’а: решение для мониторинга открытых портов на хостах
- [JavaScript, Программирование] Эффектное программирование. Часть 2: генераторы в полевых условиях
Теги для поиска: #_javascript, #_node.js, #_avtorizatsija (авторизация), #_socket.io, #_avtorizatsija_polzovatelja (авторизация пользователя), #_javascript, #_node.js
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 10:47
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
На текущий момент я работаю в компании «МегаФон» тимлидом фронта. С начала 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 }));
Таким образом, схема refresh + access токен ограничивает время, на которое атакующий может получить доступ к сервису. По сравнению с одним токеном, которым злоумышленник может пользоваться неделями и никто об этом не узнает.
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 10:47
Часовой пояс: UTC + 5