Читая это, имейте в виду, что это зависит от правильной настройки Приложения и 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
файле.
По сути, он создает строку 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, который по сути позволяет нам определить обратный вызов (или, в нашем случае, маршрутизатор), который будет использоваться с заданной схемой протокола.
Например, предположим, что мы определяем протокол как 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
, чтобы справиться с этим, хотя вы можете использовать что угодно.
Вывод
Надеюсь, вы нашли это полезным! Если у вас есть вопросы, не стесняйтесь спросить. Рад слышать любые предложения по улучшению этого.