Электрон + Auth0 | Настройка аутентификации PKCE для аутентифицированного доступа к API

Читая это, имейте в виду, что это зависит от правильной настройки Приложения и Api в вашей учетной записи Auth0.

Несколько месяцев назад я столкнулся с тем, что бился головой об стену при интеграции Auth0 в приложение Electron. Основные проблемы были замечены при обработке перенаправлений в производственном приложении на OSX. Это заставило меня переосмыслить реализацию.

Поскольку приложения Electron являются собственными настольными приложениями, было сочтено целесообразным использовать аутентификацию PKCE. Это «Код авторизации [PKCE] — это разрешение OAuth 2.0, которое собственные приложения используют для доступа к API». Создаваемое приложение Electron требовало взаимодействия с API, принадлежащими другой компании.

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

В конце концов, я был очень доволен этим. Он работал отлично и не зависел от какой-либо внешней логики (Vue, React, Angular и т. Д.) Для обработки аутентификации.

Вот реализация:

Шаг 1. Создайте обработчик аутентификации

Чтобы привести в порядок файл background.js, я создал отдельный каталог для «обработчиков» и создал файл authenticator.js.

// app/src/handlers/authenticator.js
import AuthCodePKCE from '../../auth'
let authenticationWindow
let pkceAuthenticator
// Auth processes
export const authController = (ipcMain, BrowserWindow, mainWindow) => {
  const createAuthenticationWindow = () => {
    const window = new BrowserWindow({
      webPreferences: {
        webSecurity: false
      },
      parent: mainWindow,
      resizable: false,
      modal: true,
      height: 475,
      width: 300,
      show: true
    })
    // Create a new instance of the PKCE helper
    pkceAuthenticator = new AuthCodePKCE()
    window.loadURL(pkceAuthenticator.authorizationUrl())
    window.once('ready-to-show', () => window.show())
    return window
  }
ipcMain.on('authenticate_user', event => authenticationWindow = createAuthenticationWindow())
return {
    close () {
      authenticationWindow.close()
    },
    fetchToken (code) {
      pkceAuthenticator.fetchAccessToken(code)
        .then(authResult => {
          mainWindow.webContents.send('authentication-success', authResult)
          authenticationWindow.close()
        })
    }
  }
}

Здесь решаются несколько вопросов. Во-первых, мы сохраняем логику для создания окна аутентификации в единственной функции с именем createAuthenticationWindow, которая вызывается, когда ipcMain обнаруживает событие «authenticate_user». Здесь следует отметить одну вещь (хотя это необязательно): создаваемый новый BrowserWindow определен как модальный, а его родительский элемент установлен как mainWindow. Это работает очень хорошо, поскольку обрабатывает позиционирование окна, следит за тем, чтобы пользователь не выходил из окна аутентификации, и временно запрещает пользователю перемещаться по приложению.

См. также:  Улучшайте свои коммиты с помощью обычных коммитов

Во-вторых, сам authController возвращает две функции. Первый close() закрывает окно аутентификации. Второй, fetchToken(), мы будем использовать на втором шаге 3 для получения токена API после успешной аутентификации пользователя.

Третье, на что следует обратить внимание, это то, что окно аутентификации НЕ загружает локальный файл или шаблон, а представляет собой конкретный URL-адрес, предоставленный AuthCodePKCE, который импортируется в начале этого файла.

// app/src/auth.js
import crypto from 'crypto'
import * as queryString from 'query-string'
import { domain, clientId } from './utils/init'
import * as requestPromise from 'request-promise'
const base64URLEncode = (str) => {
  return str.toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '')
}
const sha256 = (buffer) => {
  return crypto.createHash('sha256').update(buffer).digest()
}
// Authorization Code with PKCE is the OAuth 2.0
// grant that native applications use in order
// to access an API.
export default class AuthCodePKCE {
  // The verifier and the challenge need to be stored
  // for subsequent requests. Make sure to keep a reference
  // to the AuthCodePKCE instance during grant flow
  constructor () {
    this.verifier = base64URLEncode(crypto.randomBytes(32))
    this.challenge = base64URLEncode(sha256(this.verifier))
  }
// Generate link to send the user to the authorization URL 
  // with the code_challenge and the method used to generate it
  authorizationUrl () {
    return `https://${domain}/authorize?${queryString.stringify({
      redirect_uri: `my-app://desktop.com/auth`,
      code_challenge: this.challenge,
      code_challenge_method: 'S256',
      audience: 'my/api',
      scope: 'read:stuff',
      response_type: 'code',
      client_id: clientId
    })}`
  }
// Exchange code for an Access Token that can be used to call your API.
  // Using the Authorization Code (code) from the previous step, you will
  // need to POST to the Token URL sending also the code_verifier
  fetchAccessToken (code) {
    return requestPromise({
      method: 'POST',
      uri: `https://${domain}/oauth/token`,
      body: {
        redirect_uri: `my-app://desktop.com/auth`,
        grant_type: 'authorization_code',
        code_verifier: this.verifier,
        client_id: clientId,
        code: code
      },
      json: true
    })
  }
}

Этот простой класс обрабатывает два основных аспекта аутентификации PKCE: создание (и хранение) запроса и верификатор. Если вы хотите узнать больше о том, что это такое, прочтите о них в Auth0 Docs. Помимо этого, здесь стоит обратить внимание на метод authorizationUrl(), который мы только что использовали в нашем authentication.js файле.

См. также:  Функция активации Swish с большей эффективностью памяти

По сути, он создает строку URL, которая указывает на Auth0 в https://<auth0-domain>/authorize? и имеет ряд обязательных / необязательных параметров запроса. А именно, эти параметры запроса включают вызов кода и метод, с помощью которого был создан запрос кода. Этот URL-адрес загрузит размещенную страницу входа Auth0 в созданном нами окне аутентификации Electron (вы можете настроить эту страницу в Auth0).

Шаг 2. Использование обработчика для получения кода

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

// app/src/background.js
import { authHandler } from './handlers/authenticator.js'
import { isDev, PROTOCOL_PREFIX } from './utils/init'
import { format as formatUrl, parse as parseUrl } from 'url'
import { app, protocol, BrowserWindow, ipcMain } from 'electron'
// Helper function for creating and formatting paths
const baseUrl = (opts = {}) => {
  let base = {
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file',
    slashes: true
  }
  if (isDev) {
    base = parseUrl(process.env.WEBPACK_DEV_SERVER_URL)
  }
  return formatUrl(Object.assign(base, opts))
}
// Define protocol prefix (e.g. "my-app-protocol")
protocol.registerStandardSchemes([PROTOCOL_PREFIX])
let mainWindow
let authenticator
const createMainWindow = () => {
const window = new BrowserWindow({
    webPreferences: {
      webSecurity: false
    },
    width: 500,
    height: 500,
    show: false,
    minWidth: 780,
    minHeight: 475,
    icon: path.join(__dirname, 'build/icons/png/64x64.png')
  })
if (!isDev) createProtocol('app')
window.loadURL(baseUrl())
window.on('closed', () => mainWindow = null)
  window.once('ready-to-show', () => window.show())
  return window
}
// Use the protocolRouter to match 'auth' in request url
const protocolRouter = (request) => {
  let url = parseUrl(request.url)
  if (url.path.match(/auth\?code/)) {
    // Get code from url query and send code to ipcMain event
    authenticator.fetchToken(url.query.split('=')[1])
  }
}
app.on('ready', async () => {
  mainWindow = createMainWindow()
  protocol.registerHttpProtocol(PROTOCOL_PREFIX, protocolRouter)
  // Set up authenticator
  authenticator = authHandler(ipcMain, BrowserWindow, mainWindow)
})

После импорта authHandler и других необходимых / полезных модулей определяется новая функция с именем protocolRouter. Это можно было бы расширить еще больше, но здесь мы будем использовать его как есть. Его цель — простая функция маршрутизатора, которая вызывается всякий раз, когда к протоколу нашего приложения поступает новый запрос. Если вы посмотрите внутрь определения app.on('ready'…, вы увидите, что мы передаем этот маршрутизатор в качестве аргумента функции protocol.registerHttpProtocol. Это API-интерфейс Electron, который по сути позволяет нам определить обратный вызов (или, в нашем случае, маршрутизатор), который будет использоваться с заданной схемой протокола.

См. также:  Автоматический вход на сайт с помощью скрипта Google Apps

Например, предположим, что мы определяем протокол как PROTOCOL_PREFIX = "my-electron-app", а затем продолжаем с той же настройкой protocol.registerHttpProtocol(PROTOCOL_PREFIX, protocolRouter). Если вы затем наберете в браузере my-electron-app://something?param=1, запрос откроет настольное приложение и вызовет наш protocolRouter, предоставив ему объект запроса в качестве параметра.

Наконец, как вы можете видеть, мы настраиваем authHandler, передавая ему ipcMain, BrowserWindow и mainWindow в качестве аргументов. Он будет использовать все это при настройке (ссылочный код выше).

Шаг 3. Посмотрите, как это работает

Теперь, когда все настроено, по большей части я объясню последние моменты.

const protocolRouter = (request) => {
  let url = parseUrl(request.url)
  if (url.path.match(/auth\?code/)) {
    // Get code from url query and send code to ipcMain event
    authenticator.fetchToken(url.query.split('=')[1])
  }
}

Оглядываясь на protocolRouter в background.js файле, мы видим, что он настроен на соответствие "auth?code” в пути запроса. Это гибко настраивается при создании authenticationUrl (в данном случае установлено redirect_uri: my-app://desktop.com/auth), а Auth0 добавит параметр запроса code=<a code>. Если соответствие установлено, мы получаем значение кода и передаем его методу fetchToken(), который был определен параметром authHelper.

fetchToken (code) {
      pkceAuthenticator.fetchAccessToken(code)
        .then(authResult => {
          // Alert main window of authentication success
          mainWindow.webContents.send('auth-success', authResult)
          authenticationWindow.close()
        })
    }

На данный момент все довольно просто. fetchToken использует fetchAccessToken для получения токена API, который затем возвращает результат аутентификации. После этого вы сможете делать с результатом все, что захотите. В этом примере я отправляю его в главное окно браузера, где ipcRenderer прослушивает «auth-success».

fetchAccessToken (code) {
    return requestPromise({
      method: 'POST',
      uri: `https://${domain}/oauth/token`,
      body: {
        redirect_uri: `my-app://desktop.com/auth`,
        grant_type: 'authorization_code',
        code_verifier: this.verifier,
        client_id: clientId,
        code: code
      },
      json: true
    })
}
  • Примечание. FetchAccessToken отправляет запрос POST в конечную точку https://<auth0-domain>/oauth/token, передавая ей JSON-загрузку кода с несколькими другими значениями. Я использовал requestPromise, чтобы справиться с этим, хотя вы можете использовать что угодно.

Вывод

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

Понравилась статья? Поделиться с друзьями:
IT Шеф
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: