Обеспечьте безопасность приложения с помощью потока предоставления кода OAuth 2.0 и PKCE в Electron

Скорее всего, вы, вероятно, работали с OAuth или, по крайней мере, сталкивались с его реализациями в своем опыте разработчика, но иногда возникают определенные проблемы, с которыми мы сталкиваемся при объединении платформ, библиотек и фреймворков.

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

Почему бы не использовать неявный поток, когда Angular выполняет аутентификацию?

В этом есть смысл, правда? Мы используем архитектуру SPA на JavaScript! Просто загрузите msal.js или adal.js, чтобы не только упростить аутентификацию, но также обновить и выйти из системы! Он основан на перенаправлении, так что это означает, что нам придется либо изменить его с протокола file: // на localhost, либо настроить его как собственный app: //, чтобы перенаправить обратно на… Хорошо, теперь немного странно. Давайте сразу перейдем к связанным с этим рискам безопасности.

** Изменить - с тех пор, как я изначально написал эту статью, MSAL теперь поддерживает поток Code Grant, поэтому я настоятельно рекомендую вам это проверить. Однако, если вы предпочитаете делать что-то вручную, не стесняйтесь читать дальше!

Вот что нужно учитывать:

Однако, поскольку неявный поток не может быть защищен PKCE [RFC7636] (который требуется в разделе 8.1), использование неявного потока с собственными приложениями НЕ РЕКОМЕНДУЕТСЯ. - https://tools.ietf.org/html/rfc8252#section-8.2

Поскольку мы являемся собственным приложением (т. Е. общедоступным клиентом, т. Е. У нас нет контроля над источником через сервер), это достаточная причина, чтобы не использовать неявное в этой ситуации. Хорошее объяснение также дано здесь, в Security stackexchange для аналогичного вопроса, связанного с PKCE и конфиденциальными клиентскими веб-приложениями.

Теперь, когда у нас есть учреждение, которое нам нужно использовать поток предоставления кода, приступим.

Для простоты я пропущу настройку Azure, предполагая, что у вас уже есть служба приложений и AD для реализации, а также соответствующие значения, установленные в вашем манифесте для общедоступного клиента. Если нет, то в документации Microsoft для Azure есть несколько замечательных статей, описывающих настройку Службы приложений и Active Directory.

Сначала установите Node.js.

Создайте где-нибудь каталог, cd в него. Здесь я обычно разделяю приложение и электрон на два каталога (о приложении мы поговорим в отдельной статье). Тем временем в созданном вами корневом каталоге добавьте папку / electronic.

Перейдите в электронный каталог и добавьте несколько файлов в .ts или .js, в зависимости от того, что вам удобнее. Я лично создал / src в электронном каталоге для моих файлов .ts, поэтому у меня есть папка / dist, содержащая .js компилятора TypeScript. При желании вы можете добавить файлы прямо в электронную папку.

  • index.ts (для основного процесса Electron некоторые используют вместо него main.js)
  • Authenticator.ts (процесс аутентификации)
  • express.ts (необязательно: экспресс-сервер, на котором запускается поток аутентификации)
  • config.json (необязательно: если вы хотите отделить настройки аутентификации от кода)

Теперь запустите эту команду, чтобы инициализировать npm и установить зависимости.

npm init

Он будет отвечать на ряд вопросов, как это сделал Angular CLI для настройки проекта.

Теперь добавьте электрон как dev-зависимость.

npm i -D electron

Теперь вам просто нужно добавить несколько строк в индексный файл, чтобы открыть новое окно браузера при запуске вашей электронной команды. Вот простой пример из Руководства по началу работы с электроном:

Если вы обратили внимание на путь loadFile, я загружаю приложение Angular, поскольку я расскажу об этой стороне вещей в следующей статье, но вы можете загрузить любой индексный файл или даже вместо этого используйте loadUrl.

Теперь, когда у вас есть окно браузера, давайте запустим его.

Если у вас глобально установлен электрон, вы можете запустить следующее:

electron ./

Если у вас не установлен электрон во всем мире, у вас есть два варианта:

  • Запустите его через корзину node_modules:
./node_modules/.bin/electron.cmd ./dist
  • Или создайте новый скрипт npm в вашем / electronic package.json.

И просто запустите:

npm run electron

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

Аутентификация Azure

Экспресс установка

В этом примере я буду создавать локальный экспресс-сервер для redirectUri как http://127.0.0.1:[random_port]. В других случаях вы можете использовать что-то вроде [app_name]: // callback в качестве пути перенаправления.

Примечание. Рекомендуется использовать loopback URI на основе IP, а не localhost, как указано в IETF для собственных приложений в разделе 8.3.

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

Во-первых, нам нужно установить Express в наш электронный каталог.

npm i -D express

Затем откройте экспресс-файл, и мы запустим новый сервер. Давайте также создадим метод остановки сервера, так как он нам понадобится позже.

С экспрессом мы закончили! Мы инициализируем это при запуске сервера в файле аутентификатора ниже.

Настройка аутентификатора

Перейдем к файлу аутентификатора, который мы создали ранее. Первым делом мы объявим конфигурацию, а также метод для процесса входа в систему. Мы собираемся взять информацию прямо из потока Документы Microsoft для Code Grant OAuth 2.0.

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

После того, как мы создадим loadURL в окне аутентификации, нам нужно прослушать, когда он вернется с параметрами запроса в URL-адресе, которые содержат
?code= и будут иметь предоставленный код для получения токена. из.

Хорошо, давайте разберемся с этим.

session.defaultSession.webRequest.onCompleted(
  { 
    urls: [
`http://${config.express.protocol}:${config.express.port}/?code=` 
+ '*']
  }, 
  details => {

сеанс вводится посредством импорта из electronic, и когда мы используем метод onCompleted из webRequest. Мы хотим только есть этот триггер, когда запрос завершается с ?code= в полученном пути, поэтому я добавил его в качестве фильтра. Сведения - это объект, содержащий информацию о запросе, URL-адрес которого будет использоваться для извлечения кода.

const _url = details.url.split('?')[1];
const _params = new URLSearchParams(_url);
const _accessCode = _params.get('code');

С объявлением _url нам нужна только часть URL-адреса, по которому мы можем искать, иначе new URLSearchParams вернет null при получении первого параметра (попробуйте его на window.location.href, который содержит параметр запроса! ). Мы разделяем details.url на ? и берем остальную часть строки. Затем мы настраиваем _params для поиска и, наконец, получаем от него код.

Затем, если _accessCode имеет длину, мы отправляем запрос на получение access_token. Обратите внимание, что в приведенном выше фрагменте кода tokenRequestUrl заканчивается на / oauth2 / token, а не на / oauth2 / authorize, как в предыдущем запросе кода. .

Обратите внимание, как мы не передаем client_secret в теле сообщения. Это потому, что мы используем публичный клиент и это не разрешено. Позже мы добавим PKCE для дополнительной безопасности в соответствии с Разделом 8.1 справочника IETF.

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

npm i -D request

Готовы к тестированию!
Вот как выглядит полный файл аутентификатора:

Просто для тестирования мы сделаем метод аутентификации общедоступным и запустим его из нашего основного процесса.

Потрясающие! И после входа в систему мы тоже получили токен.

И если я запускаю образец запроса GET в Postman против Graph, я получаю результаты.

Давайте добавим слои State и PKCE, чтобы немного защитить это.

Добавление состояния и PKCE к запросу токена

Давайте сначала разберемся, что каждый из них представляет.

Состояние

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

ПКСЕ

Подобно атакам с перехватчиками, PKCE (произносится как Pixy) обрабатывает их между обменом кодом и запросами access_token. code_challenge отправляется в исходном запросе на предоставление кода и кэшируется на сервере авторизации, затем code_verifier используется при получении access_token в методе POST. Затем он сравнивается с исходным верификатором на сервере авторизации, откуда злоумышленник не сможет получить запрос. Это сообщает серверу авторизации, что у нас есть не только исходный код, но и мы являемся инициатором запроса предоставления кода.
Подробнее здесь - https: // tools.ietf.org/html/rfc7636

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

const authStateIdentifier = Math.random().toString(36).substring(7);

Разбив, мы создаем случайную строку с основанием-36 (base-36), которая представляет собой любой символ 0–9 A-Z и берет подстроку, начинающуюся с позиции 7.

Показывая здесь образец, мы видим, что он выводит случайную строку длиной 5 или 6 буквенно-цифровых символов.

Добавление этого в наш запрос выглядит так:

А теперь нам просто нужно добавить его к обратному вызову webRequest.onCompleted, который мы создали ранее, а затем убедиться, что это то же самое.

Большой! Теперь давайте сделаем нечто подобное с PKCE. Сначала мы создадим code_challenge и code_verifier.

Как показано в строках 1–7, сначала мы создаем новый 32-байтовый буфер, полученный из помощника, предоставленного через NodeJS под названием crypto (поэтому запустите npm i -D crypto), который Генерирует криптостойкие псевдослучайные данные. Он передается через функцию кодировщика URL-адресов base64 и делает имя файла и строку безопасными с помощью замен. Теперь он будет использоваться в качестве нашего code_verifier, который будет использоваться для создания нашего code_challenge, а также будет отправлен для получения access_token.

Строки 9–12 предназначены для code_challenge. Мы запускаем метод sha256 и передаем code_verifier. Затем он снова использует криптографический помощник для создания хэша sha256 из выходных данных нашего верификатора, а затем мы перевариваем его, чтобы создать строку для использования в наших запросах.

Теперь, когда у нас есть эти два значения, мы можем добавить их в наши запросы. Сначала добавьте его в запрос авторизации для кода, и мы установим code_challenge_method на S256.

Затем нам просто нужно добавить code_verifier к методу POST для получения access_token, чтобы сервер аутентификации мог проверить соответствие code_challenge .

Давай проверим!

Успех!

Ради удовольствия, давайте попробуем создать новый code_verifier, который будет отправлен в POST для access_token, чтобы увидеть, сможем ли мы получить сообщение об ошибке для возврата. Это означает, что верификатор, из которого был создан code_challenge, не будет тем же самым, и поэтому он должен выйти из строя на сервере аутентификации.

Именно то, что мы ожидали.

У нас все настроено на PKCE в приложении (ну, как только я верну его обратно). В этом последнем разделе я расскажу, что и как хранить в Electron, в зависимости от связанных с безопасностью последствий.

Надежное хранение с помощью Electron

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

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

В Интернете есть много обсуждений и сообщений, в которых эти два варианта обсуждаются с различной степенью детализации, обычно это происходит между локальным / сеансовым хранилищем и файлами cookie (с соответствующими настройками):

Что ж, мы не храним конфиденциальную информацию JWT, такую ​​как утверждения от id_token, а только сохраняем доступным в памяти access_token. Из-за этого мы в основном уязвимы для CSRF (во время сеанса входа в систему). Однако у refresh_token есть свои проблемы, а именно то, что он никогда не должен храниться в веб-браузере, и поэтому Electron никогда не должен передавать эту информацию обратно в веб-приложение.

Рассмотрим подробнее варианты:

localStorage

Мы только что вкратце рассмотрели это в предыдущем абзаце, но рассмотрим некоторые дополнительные детали: Локальное и сеансовое хранилище отлично подходит для хранения как непубличных, так и базовых битов данных, однако он не только никогда не предназначался для защищенных данных, так как он подвержен атакам межсайтового скриптинга (XSS) во время сеанса без аутентификации, даже несмотря на то, что мы удаляем большую часть этой возможности, удаляя NodeJS со стороны приложения - но в недавнем прошлом были известные XSS-атаки, когда кто-то придумал способ вернуть nodeIntegration значение true. Это лучше всего подходит, когда вы можете отозвать данные удаленно (например, обновить сценарий веб-приложения, чтобы удаленно стереть элементы localStorage, если что-то пойдет не так). В этом случае мы не можем, поскольку данные находятся в пределах установленного и запущенного кода (в основном, если у нас не было принудительного автоматического обновления приложения).
Узнайте больше на OWASP.

электронный магазин

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

Keytar

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

печенье

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

Следует ли зашифровать токен? Я так не верю. Две основные причины:

  • Сможет ли пользователь расшифровать что-нибудь, просто увидев это?
  • Будет ли польза для пользователя возможность изменять значение?

Поскольку оба ответа отрицательны, шифрование не принесет нам пользы.

Я также предлагаю не хранить какие-либо данные, возвращаемые из заявлений ваших API, в файлах cookie или локальном / сеансовом хранилище любыми способами.

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

В любом случае, пойдем дальше.

Добавление файла cookie httpOnly с помощью refresh_token

Глядя на документацию по файлам cookie Electron, добавить это должно быть относительно легко.

Итак, мы взяли наш токен, JSON.stringify подняли его и использовали session.defaultSession.cookies.set метод Electron для добавления объекта cookie.

Теперь при запуске приложения создается следующий результат:

Примечание: это было сделано во время обратного вызова аутентификатором для получения access_token, поэтому сохранение этого файла cookie будет во всплывающем окне. Это важно только в том случае, если вы планируете запускать свое приложение Electron как другую комбинацию протокол: порт, чем поток аутентификации.

Однако я рекомендую сделать это, поскольку после того, как пользователь входит в систему и вы больше не показываете окно аутентификации, у них нет прямого способа получить токен из файла cookie:

  • Нет доступа к инструментам разработчика для прямого чтения файла cookie
  • Нет доступа для извлечения cookie из JS в другом протоколе / порту
  • Нет доступа к Node для удаленного отображения окна авторизации

Вам просто нужно будет написать логику, чтобы всегда закрывать и снова открывать окно аутентификации при обновлении (и установить его в скрытое).

Выход из системы

Теперь, когда у нас есть способ установить / получить файл cookie, пора подумать о обновлении и выходе из системы. Давайте быстро напишем что-нибудь для Electron Flow, и вы сможете это изменить, в зависимости от того, как вы собираетесь это называть. Поскольку дальше я расскажу, как это сделать с помощью Angular, я просто добавлю кнопку Выход в окно Electron, из которой будет запускаться метод.

Это перезапишет меню по умолчанию и отобразит только кнопку «Выход из системы», а затем, щелкнув ее, запустите метод в контексте аутентификации для выхода из системы. Посмотрим, как это выглядит.

Итак, мы инициализируем наш authWindow из предыдущего, чтобы мы могли удалить из него файл cookie. Как только этот обратный вызов происходит, мы показываем окно пользователю, загружаем URL-адрес выхода, а затем, когда загрузка завершается (выход из сервера аутентификации), мы закрываем его и останавливаем наш сервер Express. После этого вы можете делать с клиентским приложением все, что хотите, например добавлять перехватчики HTTP и вставлять заголовки авторизации.

Обновите наши токены

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

Теперь, когда у нас есть метод обновления, давайте напишем логику, когда его использовать.

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

Заключение

Мы только что рассмотрели:

  • Настройте и запустите приложение Electron
  • Выполните аутентификацию через Electron с помощью потока предоставления кода OAuth v2
  • Добавлены State и PKCE для защиты запросов на access_tokens.
  • Добавлена ​​точка хранения refresh_token
  • Добавлена ​​функция сохранения токена между сеансами.
  • И, наконец, базовая функция выхода.

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

Одна вещь, которую мы не рассмотрели, - это обновление токена во время активного сеанса. В основном это было связано с тем, что у нас действительно не было триггера (например, вызова Graph API) для сбоя и возврата необходимости обновить токен. Что вы должны сделать, так это запустить refreshAccessToken сверху, чтобы достичь тех же результатов - вам просто нужно будет передать токен обратно после ответа от POST (до или после handleTokenResponse, это не имеет значения) а затем добавьте новый в свои HTTP-запросы для использования API. Более подробно об этом будет рассказано в следующей статье о привязке всего этого к Angular-приложению. Как только это будет завершено, я внесу здесь правку и опубликую ссылку.

Вы можете найти полный репозиторий здесь:
https://github.com/jsessink/electron-angular-auth/tree/master/electron

Обратите внимание, что он также будет содержать материалы из следующей статьи.

Пишите в комментариях с вопросами, исправлениями и предложениями!