Route passkey button through browser auth
This commit is contained in:
@@ -13,6 +13,19 @@ private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||||
|
if message.name == "shdDesktopAuth" {
|
||||||
|
let origin = message.frameInfo.securityOrigin
|
||||||
|
let trustedHosts = ["shd.xyz.su", "dev-shd.xyz.su"]
|
||||||
|
guard message.frameInfo.isMainFrame,
|
||||||
|
origin.protocol.lowercased() == "https",
|
||||||
|
trustedHosts.contains(origin.host.lowercased()) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let environment: MainWindowController.Environment = origin.host.lowercased() == "dev-shd.xyz.su" ? .dev : .production
|
||||||
|
controller?.signInWithPasskeyInBrowser(for: environment)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if message.name == "shdNativePrint" {
|
if message.name == "shdNativePrint" {
|
||||||
controller?.printCurrentPage()
|
controller?.printCurrentPage()
|
||||||
}
|
}
|
||||||
@@ -123,7 +136,43 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga
|
|||||||
"""
|
"""
|
||||||
let userScript = WKUserScript(source: darkScript, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
let userScript = WKUserScript(source: darkScript, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||||
config.userContentController.addUserScript(userScript)
|
config.userContentController.addUserScript(userScript)
|
||||||
|
|
||||||
|
let passkeyBridgeScript = """
|
||||||
|
(function() {
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
var path = window.location.pathname;
|
||||||
|
while (path.length > 1 && path.endsWith('/')) {
|
||||||
|
path = path.slice(0, -1);
|
||||||
|
}
|
||||||
|
if (path !== '/login' && path !== '/login/two-factor') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = event.target instanceof Element
|
||||||
|
? event.target
|
||||||
|
: (event.target && event.target.parentElement);
|
||||||
|
if (!target || !target.closest('#btnPasskeyLogin, #twoFactorPasskey')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
var bridge = window.webkit && window.webkit.messageHandlers
|
||||||
|
? window.webkit.messageHandlers.shdDesktopAuth
|
||||||
|
: null;
|
||||||
|
if (bridge) {
|
||||||
|
bridge.postMessage('passkey-login');
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
})();
|
||||||
|
"""
|
||||||
|
config.userContentController.addUserScript(
|
||||||
|
WKUserScript(source: passkeyBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)
|
||||||
|
)
|
||||||
config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdNativePrint")
|
config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdNativePrint")
|
||||||
|
config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdDesktopAuth")
|
||||||
|
|
||||||
let container = NSView()
|
let container = NSView()
|
||||||
container.translatesAutoresizingMaskIntoConstraints = false
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -199,14 +248,18 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga
|
|||||||
updateTitle()
|
updateTitle()
|
||||||
}
|
}
|
||||||
|
|
||||||
func signInWithPasskeyInBrowser() {
|
func signInWithPasskeyInBrowser(for environment: Environment) {
|
||||||
guard authSession == nil else { return }
|
guard authSession == nil else { return }
|
||||||
|
|
||||||
|
guard environment == .dev else {
|
||||||
|
presentDesktopAuthError("Вход по passkey в приложении пока подключён только для DEV.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let verifier = try Self.randomBase64URLString(byteCount: 32)
|
let verifier = try Self.randomBase64URLString(byteCount: 32)
|
||||||
let state = try Self.randomBase64URLString(byteCount: 32)
|
let state = try Self.randomBase64URLString(byteCount: 32)
|
||||||
let challenge = Self.base64URLString(for: Data(SHA256.hash(data: Data(verifier.utf8))))
|
let challenge = Self.base64URLString(for: Data(SHA256.hash(data: Data(verifier.utf8))))
|
||||||
let environment = currentEnv
|
|
||||||
|
|
||||||
guard let authorizationURL = makeAuthorizationURL(
|
guard let authorizationURL = makeAuthorizationURL(
|
||||||
environment: environment,
|
environment: environment,
|
||||||
@@ -469,6 +522,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga
|
|||||||
authSession?.cancel()
|
authSession?.cancel()
|
||||||
authSession = nil
|
authSession = nil
|
||||||
webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdNativePrint")
|
webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdNativePrint")
|
||||||
|
webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdDesktopAuth")
|
||||||
AppDelegate.shared?.removeWindowController(self)
|
AppDelegate.shared?.removeWindowController(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,19 +118,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||||||
viewMenuItem.submenu = viewMenu
|
viewMenuItem.submenu = viewMenu
|
||||||
mainMenu.addItem(viewMenuItem)
|
mainMenu.addItem(viewMenuItem)
|
||||||
|
|
||||||
// Account Menu
|
|
||||||
let accountMenuItem = NSMenuItem()
|
|
||||||
let accountMenu = NSMenu(title: "Учётная запись")
|
|
||||||
let browserLoginItem = accountMenu.addItem(
|
|
||||||
withTitle: "Войти по passkey в системном браузере…",
|
|
||||||
action: #selector(onBrowserPasskeyLogin),
|
|
||||||
keyEquivalent: "l"
|
|
||||||
)
|
|
||||||
browserLoginItem.keyEquivalentModifierMask = [.command, .shift]
|
|
||||||
browserLoginItem.target = self
|
|
||||||
accountMenuItem.submenu = accountMenu
|
|
||||||
mainMenu.addItem(accountMenuItem)
|
|
||||||
|
|
||||||
// Environment Menu
|
// Environment Menu
|
||||||
let envMenuItem = NSMenuItem()
|
let envMenuItem = NSMenuItem()
|
||||||
let envMenu = NSMenu(title: "Контур")
|
let envMenu = NSMenu(title: "Контур")
|
||||||
@@ -213,7 +200,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||||||
@objc private func onGoForward() { currentActiveController?.goForward() }
|
@objc private func onGoForward() { currentActiveController?.goForward() }
|
||||||
@objc private func onSelectProd() { currentActiveController?.navigateToEnvironment(.production) }
|
@objc private func onSelectProd() { currentActiveController?.navigateToEnvironment(.production) }
|
||||||
@objc private func onSelectDev() { currentActiveController?.navigateToEnvironment(.dev) }
|
@objc private func onSelectDev() { currentActiveController?.navigateToEnvironment(.dev) }
|
||||||
@objc private func onBrowserPasskeyLogin() { currentActiveController?.signInWithPasskeyInBrowser() }
|
|
||||||
@objc private func onShowInspector() { currentActiveController?.showInspector() }
|
@objc private func onShowInspector() { currentActiveController?.showInspector() }
|
||||||
@objc private func onShowConsole() { currentActiveController?.showConsole() }
|
@objc private func onShowConsole() { currentActiveController?.showConsole() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
## Вход в macOS-приложении
|
## Вход в macOS-приложении
|
||||||
|
|
||||||
Пункт **«Учётная запись → Войти по passkey в системном браузере…»** открывает страницу авторизации текущего контура в браузере по умолчанию (или в Safari, если браузер по умолчанию не поддерживает такой вход). После входа приложение принимает одноразовый код и отправляет его в WKWebView, чтобы сервер создал веб-сессию для выбранного контура.
|
Клик по существующей кнопке passkey на странице входа в WKWebView перехватывается приложением и открывает тот же DEV-сайт в системном браузере. После входа callback возвращает код в приложение, а WKWebView отправляет его серверу и получает веб-сессию. Вход в приложении пока включён только для DEV, где серверный поток уже развёрнут.
|
||||||
|
|
||||||
Для работы сервер должен предоставить:
|
Серверный поток развёрнут на DEV. Клиент использует такой контракт:
|
||||||
|
|
||||||
1. `GET /desktop-auth/authorize` с параметрами `client_id=shd-macos`, `redirect_uri=shd-auth://callback`, `response_type=code`, `state`, `code_challenge` и `code_challenge_method=S256`. После обычного входа, включая passkey, сервер возвращает браузер на `shd-auth://callback?code=…&state=…`.
|
1. `GET /desktop-auth/authorize` с параметрами `client_id=shd-macos`, `redirect_uri=shd-auth://callback`, `response_type=code`, `state`, `code_challenge` и `code_challenge_method=S256`. После обычного входа, включая passkey, сервер возвращает браузер на `shd-auth://callback?code=…&state=…`.
|
||||||
2. `POST /desktop-auth/complete` с полями `code` и `code_verifier`. Сервер проверяет одноразовый код и PKCE, создаёт веб-сессию, устанавливает защищённую cookie и перенаправляет WKWebView на SHD.
|
2. `POST /desktop-auth/complete` с полями `code` и `code_verifier`. Сервер проверяет одноразовый код и PKCE, создаёт веб-сессию, устанавливает защищённую cookie и перенаправляет WKWebView на SHD.
|
||||||
|
|||||||
Reference in New Issue
Block a user