Авторизация в системах одна из ключевых частей. Можно использовать какие то мощные решения, Firebase например, или что то из множества хороших библиотек (имеются в виду для node.js). Если хочется уменьшить количество зависимостей или для самообразования - то можно написать свое. 

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

В замечательном Nginx (вероятно не только в нем) есть простая и очень эффективная возможность (auth_request) конфигурировать выполнение подзапросов на каждый запрос. Иными словами при обращении клиентов к вашему API который принимает подключения на порту (например) 30000, Nginx выполнит auth_request - авторизационный подзапрос на определяемый вами порт (например) 25000. Запускаем приложение которое слушает 25000 порт, обрабатывает запрос и возвращает данные содержащие все необходимое для аутентификации в API инициатора запроса (клиента). Эта часть общеизвестна и с более подробной информацией проблем быть не должно.

Для реализации подобной схемы с помощью node.js приложений можно использовать любой модуль реализующий http сервер, даже встроенный в node.js модуль http прекрасно подойдет с минимальным написанием кода. В примере используется собственный модуль просто для удобства. Также я не буду заострять внимание на настройке Nginx - в данном случае это не важно, в крайнем случае по ссылке приведенной выше есть примеры конфига и описание директив.

Описание
  1. Nginx получает запрос к /api

  2. Nginx выполняет подзапрос к /auth

  3. Auth берет данные из запроса и выполняет ряд действий в т.ч. (не обязательно) с использованием внешних модулей (например декодирование токена jwt полученного в cookie запроса )

  4. Внешний модуль возвращает данные в Auth 

  5. Auth возвращает ответ в Nginx который полученные данные прикрепляет дополнительно к основному запросу ( auth_request_set)

  6. Nginx выполняет первоначальный запрос к API

  7. API читает дополнительные данные и по ним аутентифицирует запрос, возвращает ответ

Это в общих чертах. Перейдем к приложению. Для удобства и простоты демонстрации используется Json Web Token (JWT), в токене хранится только уникальный идентификатор пользователя (опять же для простоты пусть будет ID пользователя из БД, но никто не мешает сделать код сессии или хеш идентификатора пользователя или что то еще)

Сама обработка внутри AUTH - это последовательность методов, принимают на вход опции (о них ниже), результат вызова предыдущего, объекты модуля http : запроса (request) и ответа (response). Самый первый метод в качестве данных получает тоже request

Ссылка на код в конце статьи приведена.

После запуска и чтения конфигураций (./configs/.), идет построение для каждого пути (end-point) своей последовательности обработки из указанных методов и опций, передаваемых им. За подробностями можно посмотреть app.js метод BuildWorkers.
Все варианты методов - обработчиков расположены по пути ./workers/ и крайне просты. Например, конфигурационный файл ./configs/api.config.js

Первый метод reqprops с параметрами properties:['headers','apikey'] будет пытаться прочитать значение в свойствах с указанными именами, а именно - (на вход поступит request) request.headers, у полученного значения ищет заголовок с именем apikey. Если значение найдено - оно возвращается из метода и будет передано в следующий метод, согласно конфигурации - req. В свою очередь метод req получив первым аргументом options из конфига, выполнит запрос по указанному пути и порту передав второй аргумент (входные данные). В данном случае это и есть декодирование jwt. Полученные данные от внешнего сервиса декодирования jwt, модульвернет для обработки последующими модулями.

{
		path: "/api/*",
		workers:[
			{	
				name:"reqprops",options:{properties:['headers','apikey']}
			},
			{
				name:"req",
				options:{port:jwt.port,path:jwt.decode_path,data:{secret: jwt_secret}				}
			},
			{
				name:"inlist",
				options:{field:"id",list:["apidev"]}
			},
			{
				name:"reqprops",options:{properties:['body']}
			}
		]
	}

Дальше вызовется метод проверки наличие значения (в данном случае от jwt вернется объект с идентификатором в поле id) в списке разрешенных. Не обязательно держать в конфигурации список, здесь может быть такой же внешний запрос к другому модулю или к БД. И последним методом в цепочке стоит все тот же reqprops но уже с другим именем свойства: body которое будет прочитано и вернется. Поскольку дальше методов в конфигурации нет, это значение будет передано в следующий обработчик самого http сервера по этому пути - но это уже совсем другой модуль. 

Поскольку в самом jwt получаемом от клиента хранится только ключ данных, то эти данные должны где то быть - за это отвечает класс Data (./data.js) И два метода: для чтения данных и записи. Внешний приложения могут в любой момент прочитать или записать эти данные через api приложения (./api/). В методах - воркерах есть метод getdata который и читает данные из Data передавая их дальше, и если следом поставить метод setdata - то эти самые данные и будут отправлены в nginx в ответ на запрос auth_request. Дальше уже в nginx эти данные устанавливаются в заголовке и читаются в конечном приложении обрабатывающем основной запрос. 

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

Ссылка на код

Спасибо что дочитали, надеюсь подход/решение окажется кому то полезным. 

P.S. Был бы благодарен за коментарии по сути, что в представленном решении можно улучшить, либо что сделано не правильно, по вашему мнению.

Комментарии (14)


  1. baldr
    24.11.2021 18:36

    А еще можно использовать, например, KeyCloak. Инструкция для nginx. Поддерживает, наверное, почти все способы входа.

    Для тех, кто не хочет связываться с таким большм комбайном (или не осилил, как я пока что), есть OAuth2-Proxy, который позволяет настроить авторизацию немного попроще.

    Плюсы - хорошо развивается, много фич. Минусы - иногда развивается "слишком" быстро и ломает обратную совместимомть при крупных релизах, не все понятно в документации.

    К чему я это - не обязательно (а часто даже вредно) писать свою собственную реализацию, а потом пытаться продать ее кому-то, если есть уже готовые решения.


    1. McRain Автор
      24.11.2021 19:08
      +5

      Я разве что то продаю?
      А логика "не создавать других решений если есть одно" вообще странная. По ней, если есть  KeyCloak то не нужнен OAuth2-Proxy ?


      1. baldr
        24.11.2021 21:32
        +1

        Для себя, конечно, можете написать. Но важно понимать минусы использования. Конечно, "фатальный недостаток" (с) есть у каждого стороннего приложения.

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

        Например, конфигурация nginx - как вы предлагаете вообще его использовать? Не мешало бы привести пример конфига. Что будет если location настроен слишком широко и каждый запрос (включая статику) тоже будет требовать авторизации? Каждый запрос будет ходить на этот сервис? Существуют решения, которые ставят, например, cookie на какой-то период чтобы избежать такого оверхеда (oauth2-proxy так делает).

        Если вы делаете API, то очень бы рекомендовалось выпустить спецификацию в виде, например, OpenAPI конфига.

        Как у вас решены (никак) вопросы с безопасностью - MITM, DDoS, etc?

        А логика "не создавать других решений если есть одно" вообще странная. По ней, если есть  KeyCloak то не нужнен OAuth2-Proxy ?

        У вас нет ни внятной документации, ни тестов, ни нормальной процедуры конфигурирования. Вы не будете же распространять тот код что у вас написан? Версия 0.0.7, да, это оправдывает. Использовать в каких-то более крупных системах это тоже преждевременно. oauth2-proxy - решение более зрелое и является альтернативой тому же KeyCloak (и другим сервисам). Между ними я могу выбирать.

        Еще раз - моя претензия именно к статье. Она сыровата и малоинформативна. Конечно же, это лично мое мнение. Простите если я слишком резко высказался. Вы сами просили комментарии.


        1. McRain Автор
          25.11.2021 08:41

          Настройка Nginx выходит за рамки данного решения - если его убрать/заменить в приложении ничего не изменится. Если же этот участок (между клиентом и приложением) не был описан - место и цель использования приложения будет не верно понято.
          Нет смысла рассматривать данное приложение как решение для энтерпрайз, не совсем понятно откуда требования такие, спецификации, тонна документации. На 300 строк кода?
          Данное приложение как раз для тех случаев, когда не хочется тратить время попытки "осилить большие комбайны".


    1. hello_my_name_is_dany
      24.11.2021 21:32
      +1

      KeyCloak больше нужен для создания SSO, когда в компании много разных сервисов со своим, условно говоря, back-office. Для микросервисной архитектуры одного приложения он особо-то и не нужен (я бы даже сказал, слишком избыточен). А вот схема, которую реализовал автор вполне себе подойдёт. Мы у себя что-то похожее реализовали для front-office


      1. McRain Автор
        25.11.2021 15:53

        Тоже на node.js и nginx реализовали ?


        1. hello_my_name_is_dany
          28.11.2021 03:57

          Ну, у нас небольшой зоопарк, поэтому связка такая
          nginx -> WebSocket Proxy (Kotlin) -> нужный микросервис (Node.js)
          Проверкой токена и пользователя занимается WebSocket Proxy, потом отдаёт нужные данные (id пользователя, метаданные и тд, данные запроса) на микросервис через брокер сообщений.


          1. McRain Автор
            28.11.2021 10:59

            Понял, спасибо.


  1. BotanDorin
    24.11.2021 21:35
    +2

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

    Из банального - каждый вызов API превращается в 2-3 вызова к (возможно) внешней системе. COTS OpenID Connect решения кешируют приватные ключи пользователя и валидируют JWT локально. Накладные расходы - локальная расшифровка base64, хеширование и проверка хеша. Никаких внешних вызовов.

    Что из себя представляет приватный ключ пользователя это тоже очень важный вопрос который автор обходит его стороной. Наивная система использует один приватный ключ для всех пользователей. Его просто хешировать, но узнав его, Чарли получит доступ ко всем пользовательским аккаунтам. Умная система реализует более комплексную политику генерации и инвалидации ключей для пользователей и групп пользователей.

    Из забавного, у автора очень наивное понимание "удобства":

    В примере используется собственный модуль просто для удобства. -- Нет, удобно следовать общепринятому стандарту и использовать готовый OpenID Connect, WebAuthn или один из сотен доступных поставщиков. Писать собственный велосипед очень увлекательно, но ни в коем случае не удобно.

    Для удобства и простоты демонстрации используется Json Web Token (JWT). -- JWT с одним полем не имеет никаких преимуществ перед обычной session cookie. Автор, удобства ради, обернул обычную куку в base64 и JSON.

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


    1. baldr
      24.11.2021 21:42

      Большое спасибо, вы более развернуто объяснили то что не смог я. Все совершенно правильно.


    1. McRain Автор
      24.11.2021 22:08

      Боюсь что авторизация это как раз не на этом этапе, а там, куда дальше уходит запрос ( на схеме "API"). В самом приложении "AUTH" только аутентфикация.

      Смысл слов "используется для удобства" - в том смысле что нет никакой привязки к нему, можно использовать вместо него что либо другое, хоть http node.js - кий, хоть express.

      Данное решение не отрицает ни один из стандартов, это именно приложение, с помощью которого можно реализовать как собственную реализацию (от банального проверки пароля) до всех других протоколов.

      JWT и cookie - это все таки разные вещи)) просто в данном случае в jwt не вкладывается ничего лишнего, это дает гибкость в оперировании данными, представьте что какой то микросервис хочет дополнить данные пользователя - ДО того как он обратиться к сервису. (просто пример)

      Про "приватный ключ" совсем не понял, честно говоря, это не из статьи?

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

      Но спасибо, хотя бы понял что статью надо дополнить и расжевать.


      1. BotanDorin
        25.11.2021 02:42

        Претензии не к подходу. Под личные хотелки можно хоть собственныую ОС написать, и у этого даже будут некоторые преимущества. Претензии к тому что на хабре лежит статья которая учит что так делать круто.

        Не круто. Автор не понимает даже чем JWT отличается от сессионных кук и зачем они вообще нужны (для этого достаточно вступление в википедии прочитать), но предлагает читателям более "удобный" костыль. Кто-то это даже лайкает, добавляет в закладки и возможно даже будет использовать.

        P.S. ")))" весьма детский аргумент. "У меня работает" тоже.


        1. McRain Автор
          25.11.2021 05:39

          Мне жаль, но, тут, вероятно, опыт сдерживает понимание.
          Попробую разъяснить,
          jwt в данном случае вообще не имеет значения, он не объект статьи и не тема. На его месте в схеме может быть хоть cookie, хоть что угодно. В статье указано что он (jwt) пустой (без полезных данных) исключительно чтобы не отвлекать, "для удобства и простоты демонстрации"., так понятнее? То есть никто не запрещает потом туда включать полезные данные.
          Но если jwt пустой, то он не становится решением равнозначным cookie, потому что никто же не будет менять решение каждый раз под определенные параметры?
          Опять же статья не о jwt, он здесь для показа возможности связки приложения с внешними данными.


  1. grossws
    25.11.2021 00:38

    Осталось надеятся что в проде вы нигде не забываете заменить XlSklNDEtGXHpbkkX6ri7Fqj на новое случайное значение. А то, знаете, misconfig всякий бывает ,)