Back

Как добавить аутентификацию в Electron-приложение

Как добавить аутентификацию в Electron-приложение

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

В этой статье описывается правильная архитектура: Authorization Code Flow с PKCE, вход через системный браузер и возврат токенов в приложение через deep link или loopback-редирект.

Ключевые выводы

  • Встроенные потоки входа через BrowserWindow нарушают RFC 8252 и открывают возможность перехвата учётных данных. Используйте системный браузер.
  • PKCE (Proof Key for Code Exchange) заменяет клиентский секрет, который Electron-приложения не могут хранить безопасно.
  • Deep linking с использованием кастомной URI-схемы или loopback-редирект возвращают код авторизации обратно в главный процесс.
  • Храните refresh-токены с помощью safeStorage, который использует механизмы управления ключами на уровне ОС (Keychain, DPAPI, libsecret).
  • Изолируйте процесс рендерера, установив contextIsolation: true, и открывайте только строго ограниченные IPC-методы через contextBridge.

Чем аутентификация в Electron отличается от веб-аутентификации

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

RFC 8252 — стандарт OAuth 2.0, разработанный специально для нативных и десктопных приложений, — прямо указывает: не используйте встроенные веб-представления для OAuth-потоков входа. Встроенный BrowserWindow может незаметно перехватывать учётные данные, и у пользователя нет возможности убедиться, что он взаимодействует с легитимным провайдером идентификации. Используйте системный браузер.

Правильная архитектура: OAuth 2.0 PKCE с Deep Linking

Рекомендуемый поток выглядит следующим образом:

  1. Пользователь нажимает «Войти» в вашем Electron-приложении.
  2. Приложение открывает системный браузер с URL авторизации.
  3. Пользователь проходит аутентификацию у вашего провайдера идентификации (Auth0, Clerk, Firebase Authentication, AWS Cognito или любого совместимого с OpenID Connect провайдера).
  4. Провайдер выполняет редирект на кастомную URI-схему вида myapp://auth/callback или на loopback-адрес вида http://127.0.0.1:PORT/callback.
  5. Главный процесс Electron перехватывает этот редирект и обменивает код авторизации на токены.
  6. Токены надёжно сохраняются с помощью safeStorage.

Поскольку Electron-приложения не могут безопасно хранить клиентский секрет, PKCE (Proof Key for Code Exchange) полностью его заменяет. До начала потока приложение генерирует случайный code_verifier, хэширует его в code_challenge и отправляет challenge вместе с запросом авторизации. При обмене кода на токены отправляется исходный code_verifier. Сервер авторизации проверяет их соответствие. Никакой секрет никогда не покидает приложение.

Настройка Deep Linking для OAuth Callback

Зарегистрируйте обработчик кастомного протокола в главном процессе:

// main.js
const { app } = require('electron');
const path = require('path');

if (process.defaultApp) {
  if (process.argv.length >= 2) {
    app.setAsDefaultProtocolClient('myapp', process.execPath, [
      path.resolve(process.argv[1]),
    ]);
  }
} else {
  app.setAsDefaultProtocolClient('myapp');
}

// macOS: обработка deep link callback
app.on('open-url', (event, callbackUrl) => {
  event.preventDefault();
  handleAuthCallback(callbackUrl); // разбор кода, обмен на токены
});

// Windows и Linux: deep link приходит через second-instance
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (event, argv) => {
    const callbackUrl = argv.find((arg) => arg.startsWith('myapp://'));
    if (callbackUrl) handleAuthCallback(callbackUrl);
  });
}

На Windows и Linux событие second-instance передаёт deep link в виде аргумента командной строки. На macOS для этого используется событие open-url. При сборке пакета настройте протокол в electron-builder:

{
  "protocols": {
    "name": "myapp",
    "schemes": ["myapp"]
  }
}

Примечание: На macOS и Linux deep linking надёжно работает, как правило, только в собранных пакетах, поскольку обработчик протокола должен быть зарегистрирован в операционной системе. Всегда тестируйте deep linking на собранной версии приложения, а не просто запуская electron ..

Безопасное хранение токенов с помощью Electron safeStorage

Никогда не храните токены в localStorage, обычных файлах или electron-store без шифрования. Используйте safeStorage, который шифрует данные с помощью механизмов управления ключами на уровне ОС (Keychain на macOS, DPAPI на Windows, libsecret на Linux):

const { app, safeStorage } = require('electron');
const fs = require('fs');
const path = require('path');

const TOKEN_PATH = path.join(app.getPath('userData'), 'refresh.enc');

async function saveRefreshToken(token) {
  if (!safeStorage.isEncryptionAvailable()) {
    throw new Error('Encryption is not available on this system');
  }

  const encrypted = await safeStorage.encryptStringAsync(token);
  fs.writeFileSync(TOKEN_PATH, encrypted);
}

async function loadRefreshToken() {
  if (!fs.existsSync(TOKEN_PATH)) return null;

  const encrypted = fs.readFileSync(TOKEN_PATH);
  return await safeStorage.decryptStringAsync(encrypted);
}

Всегда проверяйте safeStorage.isEncryptionAvailable() перед шифрованием, особенно на Linux, где системное хранилище ключей может быть не настроено. На некоторых Linux-системах Electron может переключиться на бэкенд basic_text, если защищённое хранилище ключей недоступно. Вы можете проверить активный бэкенд с помощью safeStorage.getSelectedStorageBackend() и при необходимости уведомить пользователей.

Изоляция процесса рендерера

Процесс рендерера не должен иметь прямого доступа к токенам. Используйте contextIsolation, nodeIntegration: false и открывайте только необходимое через строго ограниченный contextBridge:

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('auth', {
  login: () => ipcRenderer.invoke('auth:login'),
  logout: () => ipcRenderer.send('auth:logout'),
  getProfile: () => ipcRenderer.invoke('auth:get-profile'),
});

Главный процесс выполняет все операции с токенами. Рендерер лишь вызывает именованные IPC-методы. Это ограничивает область возможного ущерба в случае компрометации рендерера вредоносным контентом.

Выбор провайдера идентификации

Описанная архитектура работает с любым совместимым с OpenID Connect провайдером. Среди популярных вариантов для Electron-приложений — Auth0, Clerk, Firebase Authentication и AWS Cognito. Зарегистрируйте своё приложение как Native или Desktop в выбранном провайдере и укажите кастомную URI-схему в качестве разрешённого redirect URI.

Паттерн, которого следует избегать

Во многих руководствах до сих пор встречается следующий подход:

// ❌ Не делайте так
const authWindow = new BrowserWindow({ /* ... */ });
authWindow.loadURL(authorizationUrl);
authWindow.webContents.session.webRequest.onBeforeRequest(
  { urls: ['http://localhost/callback*'] },
  ({ url }) => { /* перехват токенов */ }
);

Такой подход встраивает поток входа внутрь Electron, что нарушает RFC 8252 и лишает пользователя возможности проверить подлинность провайдера идентификации. Избегайте его, сколько бы руководств ни рекомендовало обратное.

Заключение

Правильный способ добавить аутентификацию в Electron-приложение прост: откройте системный браузер, используйте OAuth 2.0 с PKCE, обработайте callback через deep linking или loopback-редирект и сохраните токены с помощью safeStorage. Изолируйте процесс рендерера от любой логики работы с токенами. Именно такую архитектуру рекомендует RFC 8252, и именно её ожидают современные провайдеры, такие как Auth0 и Clerk, при регистрации нативного приложения.

Часто задаваемые вопросы

Да. Loopback-редирект использует локальный HTTP-сервер на 127.0.0.1 со случайным портом для получения OAuth-callback. Он не требует регистрации протокола на уровне ОС и хорошо подходит для разработки. Недостаток в том, что сервер необходимо запускать и останавливать при каждом входе, а некоторые провайдеры требуют явной настройки loopback URI в своей панели управления.

Да. Даже если провайдер выдаёт клиентский секрет, не стоит встраивать его в бинарный файл Electron, поскольку пользователи могут его извлечь. PKCE защищает обмен кода авторизации независимо от наличия секрета. Зарегистрируйте приложение как нативный или публичный клиент, чтобы PKCE был обязательным, а секрет не требовался.

Храните только refresh-токен с помощью safeStorage. По истечении срока действия access-токена главный процесс отправляет refresh-токен на token endpoint провайдера и получает новый access-токен. Храните access-токены в памяти главного процесса, но никогда — в рендерере. Если refresh-токен отозван или истёк, предложите пользователю войти повторно через системный браузер.

Обработчики кастомных протоколов должны быть зарегистрированы в операционной системе, и эта регистрация обычно происходит при установке или сборке пакета приложения. При запуске через electron . запускается непосредственно бинарный файл Electron, поэтому ОС не знает, что ваше приложение обрабатывает схему myapp://. Всегда тестируйте deep linking на собранной версии, полученной с помощью electron-builder или аналогичного инструмента.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay