Всем привет!
В прошлой статье я описал архитектуру сервера и рассказал, как устроено само приложение, которое обслуживает клиентов игры. В конце статьи было голосование, о каких модулях приложения было бы интересно прочитать и голоса в основном разделились между модулями "авторизация" и "игра".
Я решил начать с модуля авторизации, так как он, на мой взгляд, является базовым для любого проекта. В первой статье, я немного затрагивал эту тему, а в этот раз хочу разложить все по полочкам.

Первое с чего начинается взаимодействие пользователя с системой это получение ключа доступа для последующей авторизации, у меня это ключ в формате JSON Web Token (JWT). Ключ содержит в себе набор данных, позволяющих идентифицировать пользователя на стороне сервера. Это accessToken, который, как видно из названия, используется для доступа к закрытым методам различных модулей системы, которые требуют авторизованного доступа, например без этого ключа невозможно создать новую игру или запросить данные профиля. В классической реализации программных интерфейсов (API) между клиентом и сервером, клиенты отправляют такой ключ вместе с каждым запросом на сервер в заголовке (header) Authorization. В этом заголовке часто используется еще приставка "Bearer" с пробелом перед самим ключом, но это на самом деле не так важно, просто термин "Bearer authorization" по сути означает "Разрешение на предъявителя" и многие используют его для того чтобы сказать, что авторизация работает по данному принципу. Некоторые разработчики используют свои приставки вместо "Bearer", например "X-API" или что-то еще, но это не меняет сути, после этого всегда идет ключ и не важно JWT это или какой либо другой вариант ключа. Кстати на стороне сервера я использую полюбившуюся мне библиотеку Jose. Данная библиотека предоставляет доступ к целому спектру технологий для создания и проверки ключей, в ее арсенале JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), JSON Web Key Set (JWKS) и другие. Помимо этого ее прелесть в том, что она может работать, как на стороне сервера, так и на клиенте, к тому же поддерживается работа в разных средах для запуска JavaScript кода, таких как Node.js, Deno, Bun, а так же облачные функции, такие как Cloudflare Workers и другие.
В этом проекте у меня реализовано два варианта получения ключа доступа. Первый предусматривает использование цифрового кода, отправленного на почту которую указал пользователь в процессе аутентификации, а второй, это специальный метод для автоматической аутентификации клиента в случае запуска приложения на платформе Telegram.
Ниже представлена схема работы первого варианта.

В процессе запуска, клиентское приложение отправляет посредством HTTP транспорта запрос на обновление ключей в модуль auth, вызывая метод refresh (на схеме Case 1). В теле запроса передается идентификатор устройства (deviceId), так называемый fingerprint, который предварительно генерируется на стороне клиента. По сути это просто хеш полученный от определенного набора данных, которые предоставляет браузер о себе и о системе, где он запущен. Помимо этого данный запрос автоматически передает в заголовке (header) Cookie refreshToken, если таковой был установлен сервером в предыдущем сеансе работы приложения. На стороне сервера, в методе refresh проверяется полученная информация и происходит поиск связки этого ключа и идентификатора устройства в базе данных. В зависимости от нагруженности проекта следует выбрать наиболее подходящий вариант для хранения - это может быть PostgreSQL, Redis или что-то другое в зависимости от используемого стека и предпочтений. Если связка не найдена в хранилище, либо срок жизни ключа истек, метод возвращает ошибку с соответствующим кодом в ответе. В случае обнаружения связки с не истекшим сроком жизни метод отправляет клиенту сгенерированный accessToken и устанавливает через заголовок (header) Set-Cookie значение нового refreshToken, который будет передаваться на сервер каждый раз, при последующих запросах. Тут стоит отметить тот факт, что cookie содержащий refreshToken устанавливается с параметром "http-only", что не позволяет получить его значение со стороны браузера программным путем и именно таким образом реализовано безопасное хранение refreshToken на стороне клиента.

В случае получения ошибки, при вызове метода refresh, клиентское приложение отображает экран аутентификации пользователя, где ему предлагают ввести а��рес электронной почты для отправки кода. После ввода адреса приложение отправляет посредством HTTP транспорта запрос в модуль auth, вызывая метод getCode (на схеме Case 2). В теле запроса передается адрес почты (email) и используемый язык (language), который автоматически берется из настроек системы на стороне браузера либо может быть непосредственно указан через интерфейс приложения. На стороне сервера, в методе getCode проверяется нахождение параметра language в списке доступных языков и в случае его отсутствия там, значение заменяется на английский. Далее сервер проверяет корректность почтового адреса и в случае ошибки, отправляет клиенту ответ с соответствующим кодом. Если адрес является корректным, метод преобразует все его символы в нижний регистр и ищет адрес в базе данных. В случае отсутствия данного адреса происходит процесс регистрация нового пользователя в системе. Ему назначаются базовые права, прописываются все необходимые параметры и полученные данные возвращаются в метод getCode, где уже генерируется код авторизации, состоящий из 6 цифр и выставляется срок жизни этого кода. На следующем этапе сервер ставит в очередь на отправку шаблон письма с кодом авторизации и параметром language, чтобы пользователь получил письмо на указанном языке и отправляется ответ об успешном завершении операции. На стороне клиентского приложения отображается форма ввода кода подтверждения и после получения письма пользователь вводит код и приложение отправляет HTTP запрос в модуль auth, вызывая метод withCode, в который передается тот же адрес электронной почты, код из письма и идентификатор устройства. Сервер в свою очередь проверяет адрес почты, приводит символы в нижний регистр, производит поиск записи с этим адресом в базе данных и в случае нахождения проверяет соответствие кода и срок его жизни. Если переданные данные не корректны, либо код не соответствует записи в базе или его срок жизни истек, сервер отправляет клиенту ответ с соответствующим кодом ошибки. В противном случае сервер генерирует ключи и отправляет такой же ответ, как и в конце первого сценария (Case 1).
Как я уже писал выше, помимо варианта аутентификации с помощью кода отправляемого на почту, в рамках этого проекта был реализован еще один метод, специально под платформу Telegram Mini Apps. Схема работы этого варианта представлена ниже.

Относительно недавно, Telegram объявил о запуске своей платформы мини приложений, которая дает возможность получить доступ к огромному количеству пользователей самого Telegram за счет публикации приложения в магазине. Также они представили новый тип мини приложений, который невероятно легко интегрировать в их платформу.
Мини приложение Telegram это по сути веб приложение, запущенное в WebView самого приложения Telegram, что тоже по сути является браузером. Благодаря этому механизму и скрипту, который необходимо разместить в html коде приложения, разработчик получает доступ к данным о пользователе Telegram (и многому другому), которые размещаются в глобальном объекте window.Telegram.WebApp. Однако эти данные могут быть скомпрометированы на пути к серверу и по этому необходимо реализовать их проверку. Для этих целей на стороне сервера реализован метод withTelegramAccount, который также находится в модуле авторизация (auth).
В процессе запуска, клиентское приложение проверяет наличие этих данных в глобальной области видимости и в случае их обнаружения отправляет на сервер используя HTTP. Со своей стороны сервер проверяет эти данные с помощью специальной системы подписей, использующей ключ от бота, который является владельцем приложения.
Проверив подпись и убедившись, что полученная информация корректна, сервер проверяет наличие в базе пользователя с идентификатором пользователя Telegram, если пользователь не найден, то происходит стандартная процедура его регистрации и затем, как и в случае обнаружения такого пользователя в базе, приложение генерирует ключи (accessToken и refreshToken) и отправляет их в ответе клиенту, а в случае обнаружения каких либо ошибок отправляется сообщение с соответствующим кодом ошибки.
Данный механизм позволяет очень быстро интегрировать аутентификацию пользователя Telegram с использованием уже существующих методов модуля авторизации. В дальнейшем подобный механизм бу��ет использоваться и при реализации аутентификации пользователей с помощью протокола OAuth2.0 для пользователей имеющих аккаунт Google или Apple.
На этом описание модуля авторизации я заканчиваю и предлагаю задавать вопросы не только мне в телеграм, но и в комментариях к статье. Наверняка есть какие-то моменты которые стоит описать подробнее. Например я уже несколько лет сознательно перестал использовать пароли для доступа к аккаунту и использую исключительно только одноразовые коды и провайдеров OAuth. Это позволяет избежать кражи паролей в силу их отсутствия и таким образом существенно повысить безопасность. В некоторых своих проектах я использовал также систему двухфакторной авторизации (2FA), когда после ввода пароля или кода необходимо использовать генератор кодов для получения доступа, однако в этом случае необходимо совершать куда больше действий и реализация такой системы мне кажется избыточной в этом проекте.
В этот раз голосования не будет, потому что следующая тема статьи - модуль сервера "игра".
PS В игре добавлена возможность смены изображения аватара и доски, а также реализован интерфейс для создания своих досок с разнообразным дизайном, который пока доступен только пользователям с правами "designer". Поиграть в нарды с друзьями или проверить себя в битве с ИИ можно как всегда по ссылке.