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