From 1556470bcbaaca4954460ca06c8695e70d6a6e53 Mon Sep 17 00:00:00 2001 From: bulava92 Date: Wed, 28 Oct 2026 15:39:27 +0300 Subject: [PATCH] Route passkey button through browser auth --- Apple/Sources/SHD/MainWindowController.swift | 58 +++++++++++++++++++- Apple/Sources/SHD/main.swift | 14 ----- README.md | 4 +- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Apple/Sources/SHD/MainWindowController.swift b/Apple/Sources/SHD/MainWindowController.swift index 2986156..1a4b061 100644 --- a/Apple/Sources/SHD/MainWindowController.swift +++ b/Apple/Sources/SHD/MainWindowController.swift @@ -13,6 +13,19 @@ private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { } 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" { controller?.printCurrentPage() } @@ -123,7 +136,43 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga """ let userScript = WKUserScript(source: darkScript, injectionTime: .atDocumentStart, forMainFrameOnly: false) 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: "shdDesktopAuth") let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false @@ -199,14 +248,18 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga updateTitle() } - func signInWithPasskeyInBrowser() { + func signInWithPasskeyInBrowser(for environment: Environment) { guard authSession == nil else { return } + guard environment == .dev else { + presentDesktopAuthError("Вход по passkey в приложении пока подключён только для DEV.") + return + } + do { let verifier = 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 environment = currentEnv guard let authorizationURL = makeAuthorizationURL( environment: environment, @@ -469,6 +522,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate, WKNaviga authSession?.cancel() authSession = nil webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdNativePrint") + webView.configuration.userContentController.removeScriptMessageHandler(forName: "shdDesktopAuth") AppDelegate.shared?.removeWindowController(self) } diff --git a/Apple/Sources/SHD/main.swift b/Apple/Sources/SHD/main.swift index 1b20b4a..1750084 100644 --- a/Apple/Sources/SHD/main.swift +++ b/Apple/Sources/SHD/main.swift @@ -118,19 +118,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent viewMenuItem.submenu = viewMenu 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 let envMenuItem = NSMenuItem() let envMenu = NSMenu(title: "Контур") @@ -213,7 +200,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent @objc private func onGoForward() { currentActiveController?.goForward() } @objc private func onSelectProd() { currentActiveController?.navigateToEnvironment(.production) } @objc private func onSelectDev() { currentActiveController?.navigateToEnvironment(.dev) } - @objc private func onBrowserPasskeyLogin() { currentActiveController?.signInWithPasskeyInBrowser() } @objc private func onShowInspector() { currentActiveController?.showInspector() } @objc private func onShowConsole() { currentActiveController?.showConsole() } } diff --git a/README.md b/README.md index 3b0681b..dc70208 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ ## Вход в 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=…`. 2. `POST /desktop-auth/complete` с полями `code` и `code_verifier`. Сервер проверяет одноразовый код и PKCE, создаёт веб-сессию, устанавливает защищённую cookie и перенаправляет WKWebView на SHD.