diff options
author | Anu Aliyas <anu.aliyas@qt.io> | 2023-05-12 13:32:12 +0200 |
---|---|---|
committer | Anu Aliyas <anu.aliyas@qt.io> | 2023-07-12 17:20:57 +0200 |
commit | 605b0b3dcce24ff82c1e7a1ab3db7dace9668b81 (patch) | |
tree | e688549a6ef361f243ec82a406abb875ae0ee371 | |
parent | a3452104907874f4a3ffee83ec99c639004405e6 (diff) |
Support FIDO2 user verification
- Implemented AuthenticatorRequestClientDelegateQt support to handle authenticator requests.
- Added FIDO user verification and resident credential support
Fixes: QTBUG-90938
Fixes: QTBUG-90941
Change-Id: I6367791e1e9e8aaac27c376408377f838832f426
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Michael BrĂ¼ning <michael.bruning@qt.io>
36 files changed, 2398 insertions, 3 deletions
diff --git a/examples/webenginequick/quicknanobrowser/BrowserWindow.qml b/examples/webenginequick/quicknanobrowser/BrowserWindow.qml index d5ae19f54..bfbe13f6d 100644 --- a/examples/webenginequick/quicknanobrowser/BrowserWindow.qml +++ b/examples/webenginequick/quicknanobrowser/BrowserWindow.qml @@ -607,6 +607,9 @@ ApplicationWindow { featurePermissionDialog.feature = feature; featurePermissionDialog.visible = true; } + onWebAuthUXRequested: function(request) { + webAuthDialog.init(request); + } Timer { id: reloadTimer @@ -759,6 +762,11 @@ ApplicationWindow { anchors.fill: parent } + WebAuthDialog { + id: webAuthDialog + visible: false + } + function onDownloadRequested(download) { downloadView.visible = true; downloadView.append(download); diff --git a/examples/webenginequick/quicknanobrowser/CMakeLists.txt b/examples/webenginequick/quicknanobrowser/CMakeLists.txt index c1f0080bb..8a1a9b706 100644 --- a/examples/webenginequick/quicknanobrowser/CMakeLists.txt +++ b/examples/webenginequick/quicknanobrowser/CMakeLists.txt @@ -53,6 +53,7 @@ set(resources_resource_files "DownloadView.qml" "FindBar.qml" "FullScreenNotification.qml" + "WebAuthDialog.qml" ) qt_add_resources(quicknanobrowser "resources" diff --git a/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml b/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml new file mode 100644 index 000000000..4af401237 --- /dev/null +++ b/examples/webenginequick/quicknanobrowser/WebAuthDialog.qml @@ -0,0 +1,287 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtWebEngine + +Dialog { + id: webAuthDialog + anchors.centerIn: parent + width: Math.min(browserWindow.width, browserWindow.height) / 3 * 2 + contentWidth: verticalLayout.width +10; + contentHeight: verticalLayout.height +10; + standardButtons: Dialog.Cancel | Dialog.Apply + title: "WebAuth Request" + + property var selectAccount; + property var authrequest: null; + + Connections { + id: webauthConnection + ignoreUnknownSignals: true + function onStateChanged(state) { + webAuthDialog.setupUI(state); + } + } + + onApplied: { + switch (webAuthDialog.authrequest.state) { + case WebEngineWebAuthUXRequest.CollectPIN: + webAuthDialog.authrequest.setPin(pinEdit.text); + break; + case WebEngineWebAuthUXRequest.SelectAccount: + webAuthDialog.authrequest.setSelectedAccount(webAuthDialog.selectAccount); + break; + default: + break; + } + } + + onRejected: { + webAuthDialog.authrequest.cancel(); + } + + function init(request) { + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + webAuthDialog.authrequest = request; + webauthConnection.target = request; + setupUI(webAuthDialog.authrequest.state) + webAuthDialog.visible = true; + pinEntryError.visible = false; + } + + function setupUI(state) { + switch (state) { + case WebEngineWebAuthUXRequest.SelectAccount: + setupSelectAccountUI(); + break; + case WebEngineWebAuthUXRequest.CollectPIN: + setupCollectPIN(); + break; + case WebEngineWebAuthUXRequest.FinishTokenCollection: + setupFinishCollectToken(); + break; + case WebEngineWebAuthUXRequest.RequestFailed: + setupErrorUI(); + break; + case WebEngineWebAuthUXRequest.Completed: + webAuthDialog.close(); + break; + } + } + + ButtonGroup { + id : selectAccount; + exclusive: true; + } + + ListModel { + id: selectAccountModel + + } + contentItem: Item { + ColumnLayout { + id : verticalLayout + spacing : 10 + + Label { + id: heading + text: ""; + } + + Label { + id: description + text: ""; + } + + Row { + spacing : 10 + Label { + id: pinLabel + text: "PIN"; + } + TextInput { + id: pinEdit + text: "EnterPin" + enabled: true + focus: true + color: "white" + layer.sourceRect: { + Rectangle: { + width: 20 + height: 20 + color: "#00B000" + } + } + } + } + + Row { + spacing : 10 + Label { + id: confirmPinLabel + text: "Confirm PIN"; + } + TextEdit { + id: confirmPinEdit + text: "" + } + } + + Label { + id: pinEntryError + text: ""; + } + + Repeater { + id : selectAccountRepeater + model: selectAccountModel + Column { + spacing : 5 + RadioButton { + text: modelData + ButtonGroup.group : selectAccount; + onClicked: function(){ + webAuthDialog.selectAccount = text; + } + } + } + } + } + } + + function setupSelectAccountUI() { + webAuthDialog.selectAccount = ""; + heading.text = "Choose a passkey"; + description.text = "Which passkey do you want to use for " + webAuthDialog.authrequest.relyingPartyId; + + selectAccountModel.clear(); + var userNames = webAuthDialog.authrequest.userNames; + for (var i = 0; i < userNames.length; i++) { + selectAccountModel.append( {"name" : userNames[i]}); + } + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = true; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupCollectPIN() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + + pinEdit.clear(); + + if (requestInfo.reason === WebEngineWebAuthUXRequest.Challenge) { + heading.text = "PIN required"; + description.text = "Enter the PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + } else if (reason === WebEngineWebAuthUXRequest.Set) { + heading.text = "Set PIN "; + description.text = "Set new PIN for your security key"; + pinLabel.visible = true; + pinEdit.visible = true; + confirmPinLabel.visible = true; + confirmPinEdit.visible = true; + } + pinEntryError.text = getPINErrorDetails() + " " + requestInfo.remainingAttempts + " attempts reamining"; + pinEntryError.visible = true; + selectAccountModel.clear(); + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + standardButton(Dialog.Apply).visible = true; + } + + function getPINErrorDetails() { + var requestInfo = webAuthDialog.authrequest.pinRequest; + switch (requestInfo.error) { + case WebEngineWebAuthUXRequest.NoError: + return ""; + case WebEngineWebAuthUXRequest.TooShort: + return "Too short"; + case WebEngineWebAuthUXRequest.InternalUvLocked: + return "Internal Uv Locked"; + case WebEngineWebAuthUXRequest.WrongPIN: + return "Wrong PIN"; + case WebEngineWebAuthUXRequest.InvalidCharacters: + return "Invalid Characters"; + case WebEngineWebAuthUXRequest.SameAsCurrentPIN: + return "Same as current PIN"; + } + } + + function getRequestFailureResaon() { + var requestFailureReason = webAuthDialog.authrequest.requestFailureReason; + switch (requestFailureReason) { + case WebEngineWebAuthUXRequest.Timeout: + return " Request Timeout"; + case WebEngineWebAuthUXRequest.KeyNotRegistered: + return "Key not registered"; + case WebEngineWebAuthUXRequest.KeyAlreadyRegistered: + return "You already registered this device. You don't have to register it again + Try agin with different key or device"; + case WebEngineWebAuthUXRequest.SoftPINBlock: + return "The security key is locked because the wrong PIN was entered too many times. + To unlock it, remove and reinsert it."; + case WebEngineWebAuthUXRequest.HardPINBlock: + return "The security key is locked because the wrong PIN was entered too many times. + You'll need to reset the security key."; + case WebEngineWebAuthUXRequest.AuthenticatorRemovedDuringPINEntry: + return "Authenticator removed during verification. Please reinsert and try again"; + case WebEngineWebAuthUXRequest.AuthenticatorMissingResidentKeys: + return "Authenticator doesn't have resident key support"; + case WebEngineWebAuthUXRequest.AuthenticatorMissingUserVerification: + return "Authenticator missing user verification"; + case WebEngineWebAuthUXRequest.AuthenticatorMissingLargeBlob: + return "Authenticator missing Large Blob support"; + case WebEngineWebAuthUXRequest.NoCommonAlgorithms: + return "No common Algorithms"; + case WebEngineWebAuthUXRequest.StorageFull: + return "Storage full"; + case WebEngineWebAuthUXRequest.UserConsentDenied: + return "User consent denied"; + case WebEngineWebAuthUXRequest.WinUserCancelled: + return "User cancelled request"; + } + } + + function setupFinishCollectToken() { + heading.text = "Use your security key with " + webAuthDialog.authrequest.relyingPartyId; + description.text = "Touch your security key again to complete the request."; + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Cancel" + } + + function setupErrorUI() { + heading.text = "Something went wrong"; + description.text = getRequestFailureResaon(); + pinLabel.visible = false; + pinEdit.visible = false; + confirmPinLabel.visible = false; + confirmPinEdit.visible = false; + selectAccountModel.clear(); + pinEntryError.visible = false; + standardButton(Dialog.Apply).visible = false; + standardButton(Dialog.Cancel).visible = true; + standardButton(Dialog.Cancel).text ="Close" + } +} diff --git a/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc b/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc index e99642661..21313c1a4 100644 --- a/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc +++ b/examples/webenginequick/quicknanobrowser/doc/src/quicknanobrowser.qdoc @@ -143,6 +143,30 @@ \skipto Escape \printuntil /^\ {4}\}/ + \section1 Handling WebAuth/FIDO UX Requests + + We use the \c onWebAuthUXRequested() signal handler to handle requests for + WebAuth/FIDO UX. The \c request parameter is an instance of WebEngineWebAuthUXRequest + which contains UX request details and APIs required to process the request. + We use it to construct WebAuthUX dialog and initiates the UX request flow. + + \quotefromfile webenginequick/quicknanobrowser/BrowserWindow.qml + \skipto onWebAuthUXRequested + \printuntil } + + The \l WebEngineWebAuthUXRequest object periodically emits the \l + {WebEngineWebAuthUXRequest::}{stateChanged} signal to notify potential + observers of the current WebAuth UX states. The observers update the WebAuth + dialog accordingly. We use onStateChanged() signal handler to handle + state change requests. See \c WebAuthDialog.qml for an example + of how these signals can be handled. + + \quotefromfile webenginequick/quicknanobrowser/WebAuthDialog.qml + \skipto Connections + \printuntil } + \skipto function init(request) + \printuntil } + \section1 Files and Attributions The example uses icons from the Tango Icon Library: diff --git a/examples/webenginequick/quicknanobrowser/resources.qrc b/examples/webenginequick/quicknanobrowser/resources.qrc index 9d1f927d3..0a0b42bbb 100644 --- a/examples/webenginequick/quicknanobrowser/resources.qrc +++ b/examples/webenginequick/quicknanobrowser/resources.qrc @@ -6,6 +6,7 @@ <file>DownloadView.qml</file> <file>FindBar.qml</file> <file>FullScreenNotification.qml</file> + <file>WebAuthDialog.qml</file> </qresource> <qresource prefix="/icons"> <file alias="go-next.png">icons/3rdparty/go-next.png</file> diff --git a/examples/webenginewidgets/simplebrowser/CMakeLists.txt b/examples/webenginewidgets/simplebrowser/CMakeLists.txt index cb0769e77..a572e53f5 100644 --- a/examples/webenginewidgets/simplebrowser/CMakeLists.txt +++ b/examples/webenginewidgets/simplebrowser/CMakeLists.txt @@ -27,6 +27,7 @@ qt_add_executable(simplebrowser webpage.cpp webpage.h webpopupwindow.cpp webpopupwindow.h webview.cpp webview.h + webauthdialog.cpp webauthdialog.h webauthdialog.ui ) if(WIN32) diff --git a/examples/webenginewidgets/simplebrowser/doc/src/simplebrowser.qdoc b/examples/webenginewidgets/simplebrowser/doc/src/simplebrowser.qdoc index f45b362df..afec5e7e0 100644 --- a/examples/webenginewidgets/simplebrowser/doc/src/simplebrowser.qdoc +++ b/examples/webenginewidgets/simplebrowser/doc/src/simplebrowser.qdoc @@ -321,6 +321,30 @@ finished or when an error occurs. See \c downloadmanagerwidget.cpp for an example of how these signals can be handled. + \section1 Managing WebAuth/FIDO UX Requests + + WebAuth UX requests are associated with \l QWebEnginePage. Whenever an authenticator + requires user interaction, a UX request is triggered on the QWebEnginePage and + the \l QWebEnginePage::webAuthUXRequested signal is emitted with + \l QWebEngineWebAuthUXRequest, which in this example is forwarded + to \c WebView::handleAuthenticatorRequired: + + \quotefromfile webenginewidgets/simplebrowser/webview.cpp + \skipto connect(page, &QWebEnginePage::webAuthUXRequested + \printline connect(page, &QWebEnginePage::webAuthUXRequested + + This method creates a WebAuth UX dialog and initiates the UX request flow. + + \quotefromfile webenginewidgets/simplebrowser/webview.cpp + \skipto void WebView::handleWebAuthUXRequested(QWebEngineWebAuthUXRequest *request) + \printuntil /^\}/ + + The \l QWebEngineWebAuthUXRequest object periodically emits the \l + {QWebEngineWebAuthUXRequest::}{stateChanged} signal to notify potential + observers of the current WebAuth UX states. The observers update the WebAuth + dialog accordingly. See \c webview.cpp and \c webauthdialog.cpp for an example + of how these signals can be handled. + \section1 Files and Attributions The example uses icons from the Tango Icon Library: diff --git a/examples/webenginewidgets/simplebrowser/simplebrowser.pro b/examples/webenginewidgets/simplebrowser/simplebrowser.pro index d4cce7738..8598d237a 100644 --- a/examples/webenginewidgets/simplebrowser/simplebrowser.pro +++ b/examples/webenginewidgets/simplebrowser/simplebrowser.pro @@ -10,7 +10,8 @@ HEADERS += \ tabwidget.h \ webpage.h \ webpopupwindow.h \ - webview.h + webview.h \ + webauthdialog.h SOURCES += \ browser.cpp \ @@ -21,7 +22,8 @@ SOURCES += \ tabwidget.cpp \ webpage.cpp \ webpopupwindow.cpp \ - webview.cpp + webview.cpp \ + webauthdialog.cpp win32 { CONFIG -= embed_manifest_exe @@ -32,7 +34,8 @@ FORMS += \ certificateerrordialog.ui \ passworddialog.ui \ downloadmanagerwidget.ui \ - downloadwidget.ui + downloadwidget.ui \ + webauthdialog.ui RESOURCES += data/simplebrowser.qrc diff --git a/examples/webenginewidgets/simplebrowser/webauthdialog.cpp b/examples/webenginewidgets/simplebrowser/webauthdialog.cpp new file mode 100644 index 000000000..f153667fe --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/webauthdialog.cpp @@ -0,0 +1,294 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "webauthdialog.h" + +#include <QVBoxLayout> +#include <QRadioButton> +#include <QLineEdit> +#include <QTextEdit> +#include <QPushButton> +#include <QWebEngineView> + +WebAuthDialog::WebAuthDialog(QWebEngineWebAuthUXRequest *request, QWidget *parent) + : QDialog(parent), uxRequest(request), uiWebAuthDialog(new Ui::WebAuthDialog) +{ + uiWebAuthDialog->setupUi(this); + + buttonGroup = new QButtonGroup(this); + buttonGroup->setExclusive(true); + + scrollArea = new QScrollArea(this); + selectAccountWidget = new QWidget(this); + scrollArea->setWidget(selectAccountWidget); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + selectAccountWidget->resize(400, 150); + selectAccountLayout = new QVBoxLayout(selectAccountWidget); + uiWebAuthDialog->m_mainVerticalLayout->addWidget(scrollArea); + selectAccountLayout->setAlignment(Qt::AlignTop); + + updateDisplay(); + + connect(uiWebAuthDialog->buttonBox, &QDialogButtonBox::rejected, this, + qOverload<>(&WebAuthDialog::onCancelRequest)); + + connect(uiWebAuthDialog->buttonBox, &QDialogButtonBox::accepted, this, + qOverload<>(&WebAuthDialog::onAcceptRequest)); + QAbstractButton *button = uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry); + connect(button, &QAbstractButton::clicked, this, qOverload<>(&WebAuthDialog::onRetry)); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); +} + +WebAuthDialog::~WebAuthDialog() +{ + QList<QAbstractButton *> buttons = buttonGroup->buttons(); + auto itr = buttons.begin(); + while (itr != buttons.end()) { + QAbstractButton *radioButton = *itr; + delete radioButton; + itr++; + } + + if (buttonGroup) { + delete buttonGroup; + buttonGroup = nullptr; + } + + if (uiWebAuthDialog) { + delete uiWebAuthDialog; + uiWebAuthDialog = nullptr; + } + + // selectAccountWidget and it's children will get deleted when scroll area is destroyed + if (scrollArea) { + delete scrollArea; + scrollArea = nullptr; + } +} + +void WebAuthDialog::updateDisplay() +{ + switch (uxRequest->state()) { + case QWebEngineWebAuthUXRequest::SelectAccount: + setupSelectAccountUI(); + break; + case QWebEngineWebAuthUXRequest::CollectPIN: + setupCollectPINUI(); + break; + case QWebEngineWebAuthUXRequest::FinishTokenCollection: + setupFinishCollectTokenUI(); + break; + case QWebEngineWebAuthUXRequest::RequestFailed: + setupErrorUI(); + break; + default: + break; + } + adjustSize(); +} + +void WebAuthDialog::setupSelectAccountUI() +{ + uiWebAuthDialog->m_headingLabel->setText(tr("Choose a Passkey")); + uiWebAuthDialog->m_description->setText(tr("Which passkey do you want to use for ") + + uxRequest->relyingPartyId() + tr("? ")); + uiWebAuthDialog->m_pinGroupBox->setVisible(false); + uiWebAuthDialog->m_mainVerticalLayout->removeWidget(uiWebAuthDialog->m_pinGroupBox); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setVisible(false); + + clearSelectAccountButtons(); + scrollArea->setVisible(true); + selectAccountWidget->resize(this->width(), this->height()); + QStringList userNames = uxRequest->userNames(); + // Create radio buttons for each name + for (const QString &name : userNames) { + QRadioButton *radioButton = new QRadioButton(name); + selectAccountLayout->addWidget(radioButton); + buttonGroup->addButton(radioButton); + } + + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setVisible(true); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Cancel)->setVisible(true); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setVisible(false); +} + +void WebAuthDialog::setupFinishCollectTokenUI() +{ + clearSelectAccountButtons(); + uiWebAuthDialog->m_headingLabel->setText(tr("Use your security key with") + + uxRequest->relyingPartyId()); + uiWebAuthDialog->m_description->setText( + tr("Touch your security key again to complete the request.")); + uiWebAuthDialog->m_pinGroupBox->setVisible(false); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setVisible(false); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setVisible(false); + scrollArea->setVisible(false); +} +void WebAuthDialog::setupCollectPINUI() +{ + clearSelectAccountButtons(); + uiWebAuthDialog->m_mainVerticalLayout->addWidget(uiWebAuthDialog->m_pinGroupBox); + uiWebAuthDialog->m_pinGroupBox->setVisible(true); + uiWebAuthDialog->m_confirmPinLabel->setVisible(false); + uiWebAuthDialog->m_confirmPinLineEdit->setVisible(false); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Next")); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setVisible(true); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Cancel)->setVisible(true); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setVisible(false); + scrollArea->setVisible(false); + + QWebEngineWebAuthPINRequest pinRequestInfo = uxRequest->pinRequest(); + + if (pinRequestInfo.reason == QWebEngineWebAuthUXRequest::PINEntryReason::Challenge) { + uiWebAuthDialog->m_headingLabel->setText(tr("PIN Required")); + uiWebAuthDialog->m_description->setText(tr("Enter the PIN for your security key")); + uiWebAuthDialog->m_confirmPinLabel->setVisible(false); + uiWebAuthDialog->m_confirmPinLineEdit->setVisible(false); + } else { + if (pinRequestInfo.reason == QWebEngineWebAuthUXRequest::PINEntryReason::Set) { + uiWebAuthDialog->m_headingLabel->setText(tr("New PIN Required")); + uiWebAuthDialog->m_description->setText(tr("Set new PIN for your security key")); + } else { + uiWebAuthDialog->m_headingLabel->setText(tr("Change PIN Required")); + uiWebAuthDialog->m_description->setText(tr("Change PIN for your security key")); + } + uiWebAuthDialog->m_confirmPinLabel->setVisible(true); + uiWebAuthDialog->m_confirmPinLineEdit->setVisible(true); + } + + QString errorDetails; + switch (pinRequestInfo.error) { + case QWebEngineWebAuthUXRequest::PINEntryError::NoError: + break; + case QWebEngineWebAuthUXRequest::PINEntryError::InternalUvLocked: + errorDetails = tr("Internal User Verification Locked "); + break; + case QWebEngineWebAuthUXRequest::PINEntryError::WrongPIN: + errorDetails = tr("Wrong Pin"); + break; + case QWebEngineWebAuthUXRequest::PINEntryError::TooShort: + errorDetails = tr("Too Short"); + break; + case QWebEngineWebAuthUXRequest::PINEntryError::InvalidCharacters: + errorDetails = tr("Invalid Characters"); + break; + case QWebEngineWebAuthUXRequest::PINEntryError::SameAsCurrentPIN: + errorDetails = tr("Same as current PIN"); + break; + } + if (!errorDetails.isEmpty()) { + errorDetails += tr(" ") + QString::number(pinRequestInfo.remainingAttempts) + + tr(" attempts remaining"); + } + uiWebAuthDialog->m_pinEntryErrorLabel->setText(errorDetails); +} + +void WebAuthDialog::onCancelRequest() +{ + uxRequest->cancel(); +} + +void WebAuthDialog::onAcceptRequest() +{ + switch (uxRequest->state()) { + case QWebEngineWebAuthUXRequest::SelectAccount: + if (buttonGroup->checkedButton()) { + uxRequest->setSelectedAccount(buttonGroup->checkedButton()->text()); + } + break; + case QWebEngineWebAuthUXRequest::CollectPIN: + uxRequest->setPin(uiWebAuthDialog->m_pinLineEdit->text()); + break; + default: + break; + } +} + +void WebAuthDialog::setupErrorUI() +{ + clearSelectAccountButtons(); + QString errorDescription; + QString errorHeading = tr("Something went wrong"); + bool isVisibleRetry = false; + switch (uxRequest->requestFailureReason()) { + case QWebEngineWebAuthUXRequest::RequestFailureReason::Timeout: + errorDescription = tr("Request Timeout"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::KeyNotRegistered: + errorDescription = tr("Key not registered"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::KeyAlreadyRegistered: + errorDescription = tr("You already registered this device." + "Try again with device"); + isVisibleRetry = true; + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::SoftPINBlock: + errorDescription = + tr("The security key is locked because the wrong PIN was entered too many times." + "To unlock it, remove and reinsert it."); + isVisibleRetry = true; + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::HardPINBlock: + errorDescription = + tr("The security key is locked because the wrong PIN was entered too many times." + " You'll need to reset the security key."); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::AuthenticatorRemovedDuringPINEntry: + errorDescription = + tr("Authenticator removed during verification. Please reinsert and try again"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::AuthenticatorMissingResidentKeys: + errorDescription = tr("Authenticator doesn't have resident key support"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::AuthenticatorMissingUserVerification: + errorDescription = tr("Authenticator missing user verification"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::AuthenticatorMissingLargeBlob: + errorDescription = tr("Authenticator missing Large Blob support"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::NoCommonAlgorithms: + errorDescription = tr("Authenticator missing Large Blob support"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::StorageFull: + errorDescription = tr("Storage Full"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::UserConsentDenied: + errorDescription = tr("User consent denied"); + break; + case QWebEngineWebAuthUXRequest::RequestFailureReason::WinUserCancelled: + errorDescription = tr("User Cancelled Request"); + break; + } + + uiWebAuthDialog->m_headingLabel->setText(errorHeading); + uiWebAuthDialog->m_description->setText(errorDescription); + uiWebAuthDialog->m_description->adjustSize(); + uiWebAuthDialog->m_pinGroupBox->setVisible(false); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Ok)->setVisible(false); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setVisible(isVisibleRetry); + if (isVisibleRetry) + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Retry)->setFocus(); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Cancel)->setVisible(true); + uiWebAuthDialog->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Close")); + scrollArea->setVisible(false); +} + +void WebAuthDialog::onRetry() +{ + uxRequest->retry(); +} + +void WebAuthDialog::clearSelectAccountButtons() +{ + QList<QAbstractButton *> buttons = buttonGroup->buttons(); + auto itr = buttons.begin(); + while (itr != buttons.end()) { + QAbstractButton *radioButton = *itr; + selectAccountLayout->removeWidget(radioButton); + buttonGroup->removeButton(radioButton); + delete radioButton; + itr++; + } +} diff --git a/examples/webenginewidgets/simplebrowser/webauthdialog.h b/examples/webenginewidgets/simplebrowser/webauthdialog.h new file mode 100644 index 000000000..352bcd0d4 --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/webauthdialog.h @@ -0,0 +1,41 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef WEBAUTHDIALOG_H +#define WEBAUTHDIALOG_H + +#include <QDialog> +#include <QButtonGroup> +#include <QScrollArea> +#include "ui_webauthdialog.h" +#include "qwebenginewebauthuxrequest.h" + +class WebAuthDialog : public QDialog +{ + Q_OBJECT +public: + WebAuthDialog(QWebEngineWebAuthUXRequest *request, QWidget *parent = nullptr); + ~WebAuthDialog(); + + void updateDisplay(); + +private: + QWebEngineWebAuthUXRequest *uxRequest; + QButtonGroup *buttonGroup = nullptr; + QScrollArea *scrollArea = nullptr; + QWidget *selectAccountWidget = nullptr; + QVBoxLayout *selectAccountLayout = nullptr; + + void setupSelectAccountUI(); + void setupCollectPINUI(); + void setupFinishCollectTokenUI(); + void setupErrorUI(); + void onCancelRequest(); + void onRetry(); + void onAcceptRequest(); + void clearSelectAccountButtons(); + + Ui::WebAuthDialog *uiWebAuthDialog; +}; + +#endif // WEBAUTHDIALOG_H diff --git a/examples/webenginewidgets/simplebrowser/webauthdialog.ui b/examples/webenginewidgets/simplebrowser/webauthdialog.ui new file mode 100644 index 000000000..c8a0456d6 --- /dev/null +++ b/examples/webenginewidgets/simplebrowser/webauthdialog.ui @@ -0,0 +1,151 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>WebAuthDialog</class> + <widget class="QDialog" name="WebAuthDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>563</width> + <height>397</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="geometry"> + <rect> + <x>20</x> + <y>320</y> + <width>471</width> + <height>32</height> + </rect> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Retry</set> + </property> + </widget> + <widget class="QLabel" name="m_headingLabel"> + <property name="geometry"> + <rect> + <x>30</x> + <y>20</y> + <width>321</width> + <height>16</height> + </rect> + </property> + <property name="text"> + <string>Heading</string> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + </widget> + <widget class="QLabel" name="m_description"> + <property name="geometry"> + <rect> + <x>30</x> + <y>60</y> + <width>491</width> + <height>31</height> + </rect> + </property> + <property name="text"> + <string>Description</string> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + </widget> + <widget class="QWidget" name="layoutWidget"> + <property name="geometry"> + <rect> + <x>20</x> + <y>100</y> + <width>471</width> + <height>171</height> + </rect> + </property> + <layout class="QVBoxLayout" name="m_mainVerticalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <item> + <widget class="QGroupBox" name="m_pinGroupBox"> + <property name="title"> + <string/> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <widget class="QLabel" name="m_pinLabel"> + <property name="geometry"> + <rect> + <x>10</x> + <y>20</y> + <width>58</width> + <height>16</height> + </rect> + </property> + <property name="text"> + <string>PIN</string> + </property> + </widget> + <widget class="QLineEdit" name="m_pinLineEdit"> + <property name="geometry"> + <rect> + <x>90</x> + <y>20</y> + <width>113</width> + <height>21</height> + </rect> + </property> + </widget> + <widget class="QLabel" name="m_confirmPinLabel"> + <property name="geometry"> + <rect> + <x>10</x> + <y>50</y> + <width>81</width> + <height>16</height> + </rect> + </property> + <property name="text"> + <string>Confirm PIN</string> + </property> + </widget> + <widget class="QLineEdit" name="m_confirmPinLineEdit"> + <property name="geometry"> + <rect> + <x>90</x> + <y>50</y> + <width>113</width> + <height>21</height> + </rect> + </property> + </widget> + <widget class="QLabel" name="m_pinEntryErrorLabel"> + <property name="geometry"> + <rect> + <x>10</x> + <y>80</y> + <width>441</width> + <height>16</height> + </rect> + </property> + <property name="text"> + <string>TextLabel</string> + </property> + </widget> + </widget> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections/> +</ui> diff --git a/examples/webenginewidgets/simplebrowser/webview.cpp b/examples/webenginewidgets/simplebrowser/webview.cpp index e024f9126..a47cb23a3 100644 --- a/examples/webenginewidgets/simplebrowser/webview.cpp +++ b/examples/webenginewidgets/simplebrowser/webview.cpp @@ -9,6 +9,7 @@ #include "webview.h" #include "ui_certificateerrordialog.h" #include "ui_passworddialog.h" +#include "webauthdialog.h" #include <QContextMenuEvent> #include <QDebug> #include <QMenu> @@ -98,6 +99,8 @@ void WebView::setPage(WebPage *page) &WebView::handleProxyAuthenticationRequired); disconnect(oldPage, &QWebEnginePage::registerProtocolHandlerRequested, this, &WebView::handleRegisterProtocolHandlerRequested); + disconnect(oldPage, &QWebEnginePage::webAuthUXRequested, this, + &WebView::handleWebAuthUXRequested); #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) disconnect(oldPage, &QWebEnginePage::fileSystemAccessRequested, this, &WebView::handleFileSystemAccessRequested); @@ -121,6 +124,7 @@ void WebView::setPage(WebPage *page) connect(page, &QWebEnginePage::fileSystemAccessRequested, this, &WebView::handleFileSystemAccessRequested); #endif + connect(page, &QWebEnginePage::webAuthUXRequested, this, &WebView::handleWebAuthUXRequested); } int WebView::loadProgress() const @@ -292,6 +296,32 @@ void WebView::handleProxyAuthenticationRequired(const QUrl &, QAuthenticator *au } } +void WebView::handleWebAuthUXRequested(QWebEngineWebAuthUXRequest *request) +{ + if (m_authDialog) + delete m_authDialog; + + m_authDialog = new WebAuthDialog(request, window()); + m_authDialog->setModal(false); + m_authDialog->setWindowFlags(m_authDialog->windowFlags() & ~Qt::WindowContextHelpButtonHint); + + connect(request, &QWebEngineWebAuthUXRequest::stateChanged, this, &WebView::onStateChanged); + m_authDialog->show(); +} + +void WebView::onStateChanged(QWebEngineWebAuthUXRequest::WebAuthUXState state) +{ + if (QWebEngineWebAuthUXRequest::Completed == state + || QWebEngineWebAuthUXRequest::Cancelled == state) { + if (m_authDialog) { + delete m_authDialog; + m_authDialog = nullptr; + } + } else { + m_authDialog->updateDisplay(); + } +} + //! [registerProtocolHandlerRequested] void WebView::handleRegisterProtocolHandlerRequested( QWebEngineRegisterProtocolHandlerRequest request) diff --git a/examples/webenginewidgets/simplebrowser/webview.h b/examples/webenginewidgets/simplebrowser/webview.h index 41bc04ac0..bb4b1f2ad 100644 --- a/examples/webenginewidgets/simplebrowser/webview.h +++ b/examples/webenginewidgets/simplebrowser/webview.h @@ -12,8 +12,10 @@ #endif #include <QWebEnginePage> #include <QWebEngineRegisterProtocolHandlerRequest> +#include <QWebEngineWebAuthUXRequest> class WebPage; +class WebAuthDialog; class WebView : public QWebEngineView { @@ -45,13 +47,16 @@ private slots: void handleRegisterProtocolHandlerRequested(QWebEngineRegisterProtocolHandlerRequest request); #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) void handleFileSystemAccessRequested(QWebEngineFileSystemAccessRequest request); + void handleWebAuthUXRequested(QWebEngineWebAuthUXRequest *request); #endif private: void createWebActionTrigger(QWebEnginePage *page, QWebEnginePage::WebAction); + void onStateChanged(QWebEngineWebAuthUXRequest::WebAuthUXState state); private: int m_loadProgress = 100; + WebAuthDialog *m_authDialog = nullptr; }; #endif diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 02d2584a6..cbcbf0100 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -196,6 +196,8 @@ foreach(arch ${archs}) web_engine_settings.cpp web_engine_settings.h web_event_factory.cpp web_event_factory.h web_usb_detector_qt.cpp web_usb_detector_qt.h + authenticator_request_client_delegate_qt.cpp authenticator_request_client_delegate_qt.h + authenticator_request_dialog_controller.cpp authenticator_request_dialog_controller.h authenticator_request_dialog_controller_p.h ) extend_gn_target(${buildGn} CONDITION QT_FEATURE_accessibility diff --git a/src/core/api/CMakeLists.txt b/src/core/api/CMakeLists.txt index 208ccf10d..564d7b382 100644 --- a/src/core/api/CMakeLists.txt +++ b/src/core/api/CMakeLists.txt @@ -39,6 +39,7 @@ qt_internal_add_module(WebEngineCore qwebengineurlscheme.cpp qwebengineurlscheme.h qwebengineurlschemehandler.cpp qwebengineurlschemehandler.h qwebengineglobalsettings.cpp qwebengineglobalsettings.h qwebengineglobalsettings_p.h + qwebenginewebauthuxrequest.cpp qwebenginewebauthuxrequest.h qwebenginewebauthuxrequest_p.h DEFINES BUILDING_CHROMIUM INCLUDE_DIRECTORIES diff --git a/src/core/api/qwebenginepage.cpp b/src/core/api/qwebenginepage.cpp index 4c0b7b8ff..0c60300e1 100644 --- a/src/core/api/qwebenginepage.cpp +++ b/src/core/api/qwebenginepage.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qwebenginepage.h" +#include "authenticator_request_dialog_controller.h" #include "qwebenginepage_p.h" #include "qwebenginecertificateerror.h" @@ -21,6 +22,7 @@ #include "qwebenginescript.h" #include "qwebenginescriptcollection_p.h" #include "qwebenginesettings.h" +#include "qwebenginewebauthuxrequest.h" #include "authentication_dialog_controller.h" #include "autofill_popup_controller.h" @@ -776,6 +778,12 @@ void QWebEnginePagePrivate::ensureInitialized() const adapter->loadDefault(); } +void QWebEnginePagePrivate::showWebAuthDialog(QWebEngineWebAuthUXRequest *request) +{ + Q_Q(QWebEnginePage); + Q_EMIT q->webAuthUXRequested(request); +} + QWebEnginePage::QWebEnginePage(QObject* parent) : QObject(parent) , d_ptr(new QWebEnginePagePrivate()) diff --git a/src/core/api/qwebenginepage.h b/src/core/api/qwebenginepage.h index d0f1c70ee..8603d1065 100644 --- a/src/core/api/qwebenginepage.h +++ b/src/core/api/qwebenginepage.h @@ -41,6 +41,7 @@ class QWebEngineScriptCollection; class QWebEngineSettings; class QWebEngineUrlRequestInterceptor; class QWebEngineUrlResponseInterceptor; +class QWebEngineWebAuthUXRequest; class Q_WEBENGINECORE_EXPORT QWebEnginePage : public QObject { @@ -355,6 +356,8 @@ Q_SIGNALS: // TODO: fixme / rewrite bindPageToView void _q_aboutToDelete(); + void webAuthUXRequested(QWebEngineWebAuthUXRequest *request); + protected: virtual QWebEnginePage *createWindow(WebWindowType type); virtual QStringList chooseFiles(FileSelectionMode mode, const QStringList &oldFiles, diff --git a/src/core/api/qwebenginepage_p.h b/src/core/api/qwebenginepage_p.h index 532a9b942..a261e1d38 100644 --- a/src/core/api/qwebenginepage_p.h +++ b/src/core/api/qwebenginepage_p.h @@ -171,6 +171,7 @@ public: void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, const QRect &bounds, bool autoselectFirstSuggestion) override; void hideAutofillPopup() override; + void showWebAuthDialog(QWebEngineWebAuthUXRequest *controller) override; QtWebEngineCore::ProfileAdapter *profileAdapter() override; QtWebEngineCore::WebContentsAdapter *webContentsAdapter() override; diff --git a/src/core/api/qwebenginewebauthuxrequest.cpp b/src/core/api/qwebenginewebauthuxrequest.cpp new file mode 100644 index 000000000..4cd9e0ad4 --- /dev/null +++ b/src/core/api/qwebenginewebauthuxrequest.cpp @@ -0,0 +1,431 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qwebenginewebauthuxrequest.h" +#include "qwebenginewebauthuxrequest_p.h" +#include "authenticator_request_dialog_controller.h" + +/*! + \qmltype WebEngineWebAuthUXRequest + \instantiates QWebEngineWebAuthUXRequest + \inqmlmodule QtWebEngine + \since QtWebEngine 6.7 + \brief Encapsulates the data of a WebAuth UX request. + + Web engine's WebAuth UX requests are passed to the user in the + \l WebEngineView::webAuthUXRequested() signal. + + For more information about how to handle web engine authenticator requests, see the + \l{WebEngine Quick Nano Browser}{Nano Browser}. +*/ + +/*! + \class QWebEngineWebAuthUXRequest + \brief The QWebEngineWebAuthUXRequest class encapsulates the data of a WebAuth UX request. + \since 6.7 + + \inmodule QtWebEngineCore + + This class contains the information and API for WebAuth UX. WebAuth may require user + interaction during the authentication process. These requests are handled by displaying a + dialog to users. QtWebEngine currently supports user verification, resident credentials, + and display request failure UX requests. + + QWebEngineWebAuthUXRequest models a WebAuth UX request throughout its life cycle, + starting with showing a UX dialog, updating it's content through state changes, and + finally closing the dialog. + + WebAuth UX requests are normally triggered when the authenticator requires user interaction. + It is the QWebEnginePage's responsibility to notify the application of the new WebAuth UX + requests, which it does by emitting the + \l{QWebEnginePage::webAuthUXRequested}{webAuthUXRequested} signal together with a newly + created QWebEngineWebAuthUXRequest. The application can then examine this request and + display a WebAuth UX dialog. + + The QWebEngineWebAuthUXRequest object periodically emits the \l + {QWebEngineWebAuthUXRequest::}{stateChanged} signal to notify potential + observers of the current WebAuth UX states. The observers update the WebAuth dialog + accordingly. + + For more information about how to handle web engine authenticator requests, see the + \l{WebEngine Widgets Simple Browser Example}{Simple Browser}. +*/ + +/*! + \struct QWebEngineWebAuthPINRequest + \brief The QWebEngineWebAuthPINRequest class encapsulates the data of a PIN WebAuth UX request. + \since 6.7 + + \inmodule QtWebEngineCore + + This encapsulates the following information related to a PIN request made by an authenticator. + \list + \li The reason for the PIN prompt. + \li The error details for the PIN prompt. + \li The number of attempts remaining before a hard lock. Should be ignored unless + \l{QWebEngineWebAuthPINRequest::reason} is + \l{QWebEngineWebAuthUXRequest::PINEntryReason::Challenge}. + \li The minimum PIN length the authenticator will accept for the PIN. + \endlist + Use this structure to update the WebAuth UX dialog when the WebAuth UX state is \l + QWebEngineWebAuthUXRequest::CollectPIN. +*/ + +/*! + \property QWebEngineWebAuthPINRequest::reason + \brief The reason for the PIN prompt. +*/ + +/*! + \property QWebEngineWebAuthPINRequest::error + \brief The error details for the PIN prompt. +*/ + +/*! + \property QWebEngineWebAuthPINRequest::remainingAttempts + \brief The number of attempts remaining before a hard lock. Should be ignored unless + \l{QWebEngineWebAuthPINRequest::reason} is + \l{QWebEngineWebAuthUXRequest::PINEntryReason::Challenge}. +*/ + +/*! + \property QWebEngineWebAuthPINRequest::minPinLength + \brief The minimum PIN length the authenticator will accept for the PIN. +*/ + +/*! + \enum QWebEngineWebAuthUXRequest::WebAuthUXState + + This enum describes the state of the current WebAuth UX request. + + \value NotStarted WebAuth UX request not started yet. + \value SelectAccount The authenticator requires resident credential details. + The application needs to display an account details dialog, and + the user needs to select an account to proceed. + \value CollectPIN The authenticator requires user verification. + The application needs to display a PIN request dialog. + \value FinishTokenCollection The authenticator requires token/user verification (like tap on + the FIDO key) to complete the process. + \value RequestFailed WebAuth request failed. Display error details. + \value Cancelled WebAuth request is cancelled. Close the WebAuth dialog. + \value Completed WebAuth request is completed. Close the WebAuth dialog. +*/ + +/*! + \enum QWebEngineWebAuthUXRequest::PINEntryReason + + This enum describes the reasons that may prompt the authenticator to ask for a PIN. + + \value Set A new PIN is being set. + \value Change The existing PIN must be changed before using this authenticator. + \value Challenge The existing PIN is being collected to prove user verification. +*/ + +/*! + \enum QWebEngineWebAuthUXRequest::PINEntryError + + This enum describes the errors that may prompt the authenticator to ask for a PIN. + + \value NoError No error has occurred. + \value InternalUvLocked Internal UV is locked, so we are falling back to PIN. + \value WrongPIN The PIN the user entered does not match the authenticator PIN. + \value TooShort The new PIN the user entered is too short. + \value InvalidCharacters The new PIN the user entered contains invalid characters. + \value SameAsCurrentPIN The new PIN the user entered is the same as the currently set PIN. +*/ + +/*! + \enum QWebEngineWebAuthUXRequest::RequestFailureReason + + This enum describes the reason for WebAuth request failure. + + \value Timeout The authentication session has timed out. + \value KeyNotRegistered Key is not registered with the authenticator. + \value KeyAlreadyRegistered Key is already registered with the authenticator. + Try to register with another Key or use another authenticator. + \value SoftPINBlock The authenticator is blocked as the user entered the wrong key many times. + \value HardPINBlock The authenticator is blocked as the user entered the wrong key many times + and reset the PIN to use the specific authenticator again. + \value AuthenticatorRemovedDuringPINEntry Authenticator removed during PIN entry. + \value AuthenticatorMissingResidentKeys Authenticator doesn't have resident key support. + \value AuthenticatorMissingUserVerification Authenticator doesn't + have user verification support. + \value AuthenticatorMissingLargeBlob Authenticator doesn't have large blob support. + \value NoCommonAlgorithms No common algorithm. + \value StorageFull The resident credential could not be created because the + authenticator has insufficient storage. + \value UserConsentDenied User consent denied. + \value WinUserCancelled The user clicked \uicontrol Cancel in the native windows UI. +*/ + +/*! + \fn void QWebEngineWebAuthUXRequest::stateChanged(WebAuthUXState state) + + This signal is emitted whenever the WebAuth UX's \a state changes. + + \sa state, WebAuthUXState +*/ + +/*! + \qmlsignal void WebEngineWebAuthUXRequest::stateChanged(WebAuthUXState state) + This signal is emitted whenever the WebAuth UX's \a state changes. + + \sa state, QWebEngineWebAuthUXRequest::WebAuthUXState +*/ + +/*! \internal + */ +QWebEngineWebAuthUXRequestPrivate::QWebEngineWebAuthUXRequestPrivate( + QtWebEngineCore::AuthenticatorRequestDialogController *controller) + : webAuthDialogController(controller) +{ + m_currentState = controller->state(); +} + +/*! \internal + */ +QWebEngineWebAuthUXRequestPrivate::~QWebEngineWebAuthUXRequestPrivate() { } + +/*! \internal + */ +void QWebEngineWebAuthUXRequest::handleUXUpdate(WebAuthUXState currentState) +{ + Q_D(QWebEngineWebAuthUXRequest); + + d->m_currentState = currentState; + + Q_EMIT stateChanged(d->m_currentState); +} + +/*! \internal + */ +QWebEngineWebAuthUXRequest::QWebEngineWebAuthUXRequest(QWebEngineWebAuthUXRequestPrivate *p) + : d_ptr(p) +{ + connect(d_ptr->webAuthDialogController, + &QtWebEngineCore::AuthenticatorRequestDialogController::stateChanged, this, + &QWebEngineWebAuthUXRequest::handleUXUpdate); +} + +/*! \internal + */ +QWebEngineWebAuthUXRequest::~QWebEngineWebAuthUXRequest() { } + +/*! + \qmlproperty stringlist WebEngineWebAuthUXRequest::userNames + \brief The available user names for the resident credential support. + + This is needed when the current WebAuth request's UX state is + WebEngineWebAuthUXRequest.SelectAccount. The WebAuth dialog displays user names. + The user needs to select an account to proceed. + + \sa state setSelectedAccount() QWebEngineWebAuthUXRequest::userNames +*/ +/*! + \property QWebEngineWebAuthUXRequest::userNames + \brief The available user names for the resident credential support. + This is needed when the current WebAuth request's UX state is \l SelectAccount. + The WebAuth dialog displays user names. The user needs to select an account to proceed. + + \sa SelectAccount setSelectedAccount() +*/ +QStringList QWebEngineWebAuthUXRequest::userNames() const +{ + const Q_D(QWebEngineWebAuthUXRequest); + + return d->webAuthDialogController->userNames(); +} + +/*! + \qmlproperty string WebEngineWebAuthUXRequest::relyingPartyId + \brief The WebAuth request's relying party id. +*/ +/*! + \property QWebEngineWebAuthUXRequest::relyingPartyId + \brief The WebAuth request's relying party id. +*/ +QString QWebEngineWebAuthUXRequest::relyingPartyId() const +{ + const Q_D(QWebEngineWebAuthUXRequest); + + return d->webAuthDialogController->relyingPartyId(); +} + +/*! + \qmlproperty QWebEngineWebAuthPINRequest WebEngineWebAuthUXRequest::pinRequest + \brief The WebAuth request's PIN request information. + + \sa QWebEngineWebAuthPINRequest +*/ +/*! + \property QWebEngineWebAuthUXRequest::pinRequest + \brief The WebAuth request's PIN request information. + + This is needed when the current WebAuth request state is \l CollectPIN. + WebAuth Dialog displays a PIN request dialog. The user needs to enter a PIN and + invoke \l setPin() to proceed. + + \sa QWebEngineWebAuthPINRequest CollectPIN setPin() +*/ +QWebEngineWebAuthPINRequest QWebEngineWebAuthUXRequest::pinRequest() const +{ + const Q_D(QWebEngineWebAuthUXRequest); + + return d->webAuthDialogController->pinRequest(); +} + +/*! + \qmlproperty enumeration WebEngineWebAuthUXRequest::state + \brief The WebAuth request's current UX state. + + \value WebEngineWebAuthUXRequest.NotStarted WebAuth UX request not started yet. + \value WebEngineWebAuthUXRequest.SelectAccount The authenticator requires + resident credential details. The application needs to display an account details dialog, + and the user needs to select an account to proceed. + \value WebEngineWebAuthUXRequest.CollectPIN The authenticator requires user verification. + The application needs to display a PIN request dialog. + \value WebEngineWebAuthUXRequest.FinishTokenCollection The authenticator requires + token/user verification (like tap on the FIDO key) to complete the process. + \value WebEngineWebAuthUXRequest.RequestFailed WebAuth request failed. Display error details. + \value WebEngineWebAuthUXRequest.Cancelled WebAuth request is cancelled. + Close the WebAuth dialog. + \value WebEngineWebAuthUXRequest.Completed WebAuth request is completed. + Close the WebAuth dialog. +*/ +/*! + \property QWebEngineWebAuthUXRequest::state + \brief The WebAuth request's current UX state. + + \l stateChanged() is emitted when the current state changes. + Update the WebAuth dialog in reponse to the changes in state. +*/ +QWebEngineWebAuthUXRequest::WebAuthUXState QWebEngineWebAuthUXRequest::state() const +{ + return d_ptr->m_currentState; +} + +/*! + \qmlmethod void WebEngineWebAuthUXRequest::setSelectedAccount(const QString &selectedAccount) + Sends the \a selectedAccount name to the authenticator. + This is needed when the current WebAuth request's UX state is + WebEngineWebAuthUXRequest.SelectAccount. The WebAuth request is blocked until the user selects + an account and invokes this method. + + \sa WebEngineWebAuthUXRequest::userNames state +*/ +/*! + Sends the \a selectedAccount name to the authenticator. + This is needed when the current WebAuth request's UX state is \l SelectAccount. + The WebAuth request is blocked until the user selects an account and invokes this method. + + \sa userNames SelectAccount +*/ +void QWebEngineWebAuthUXRequest::setSelectedAccount(const QString &selectedAccount) +{ + Q_D(QWebEngineWebAuthUXRequest); + + d->webAuthDialogController->sendSelectAccountResponse(selectedAccount); +} + +/*! + \qmlmethod void WebEngineWebAuthUXRequest::setPin(const QString &pin) + Sends the \a pin to the authenticator that prompts for a PIN. + This is needed when the current WebAuth request's UX state is + WebEngineWebAuthUXRequest.CollectPIN. The WebAuth request is blocked until + the user responds with a PIN. + + \sa QWebEngineWebAuthPINRequest state +*/ +/*! + Sends the \a pin to the authenticator that prompts for a PIN. + This is needed when the current WebAuth request's UX state is \l CollectPIN. + The WebAuth request is blocked until the user responds with a PIN. + + \sa QWebEngineWebAuthPINRequest CollectPIN +*/ +void QWebEngineWebAuthUXRequest::setPin(const QString &pin) +{ + Q_D(QWebEngineWebAuthUXRequest); + d->webAuthDialogController->sendCollectPinResponse(pin); +} + +/*! + \qmlmethod void WebEngineWebAuthUXRequest::cancel() + Cancels the current WebAuth request. + + \sa QWebEngineWebAuthUXRequest::Cancelled, WebEngineWebAuthUXRequest::stateChanged() +*/ +/*! + Cancels the current WebAuth request. + + \sa QWebEngineWebAuthUXRequest::Cancelled, stateChanged() +*/ +void QWebEngineWebAuthUXRequest::cancel() +{ + Q_D(QWebEngineWebAuthUXRequest); + + d->webAuthDialogController->reject(); +} + +/*! + \qmlmethod void WebEngineWebAuthUXRequest::retry() + Retries the current WebAuth request. + + \sa stateChanged() +*/ +/*! + Retries the current WebAuth request. + + \sa stateChanged() +*/ +void QWebEngineWebAuthUXRequest::retry() +{ + const Q_D(QWebEngineWebAuthUXRequest); + + d->webAuthDialogController->retryRequest(); +} + +/*! + \qmlproperty enumeration WebEngineWebAuthUXRequest::requestFailureReason + \brief The WebAuth request's failure reason. + + \value WebEngineWebAuthUXRequest.Timeout The authentication session has timed out. + \value WebEngineWebAuthUXRequest.KeyNotRegistered Key is not registered with the authenticator. + \value WebEngineWebAuthUXRequest.KeyAlreadyRegistered Key is already registered with + the authenticator. Try to register with another key or use another authenticator. + \value WebEngineWebAuthUXRequest.SoftPINBlock The authenticator is blocked as the user + entered the wrong key many times. + \value WebEngineWebAuthUXRequest.HardPINBlock The authenticator is blocked as the user entered + the wrong key many times and reset the PIN to use the specific authenticator again. + \value WebEngineWebAuthUXRequest.AuthenticatorRemovedDuringPINEntry Authenticator + removed during PIN entry. + \value WebEngineWebAuthUXRequest.AuthenticatorMissingResidentKeys Authenticator doesn't + have resident key support. + \value WebEngineWebAuthUXRequest.AuthenticatorMissingUserVerification Authenticator doesn't + have user verification support. + \value WebEngineWebAuthUXRequest.AuthenticatorMissingLargeBlob Authenticator doesn't have + large blob support. + \value WebEngineWebAuthUXRequest.NoCommonAlgorithms No common algorithm. + \value WebEngineWebAuthUXRequest.StorageFull The resident credential could not be created + because the authenticator has insufficient storage. + \value WebEngineWebAuthUXRequest.UserConsentDenied User consent denied. + \value WebEngineWebAuthUXRequest.WinUserCancelled The user clicked \uicontrol Cancel + in the native windows UI. + + \sa stateChanged() +*/ +/*! + \property QWebEngineWebAuthUXRequest::requestFailureReason + \brief The WebAuth request's failure reason. + + \sa stateChanged() QWebEngineWebAuthUXRequest::RequestFailureReason +*/ +QWebEngineWebAuthUXRequest::RequestFailureReason +QWebEngineWebAuthUXRequest::requestFailureReason() const +{ + const Q_D(QWebEngineWebAuthUXRequest); + + return d->webAuthDialogController->requestFailureReason(); +} + +#include "moc_qwebenginewebauthuxrequest.cpp" diff --git a/src/core/api/qwebenginewebauthuxrequest.h b/src/core/api/qwebenginewebauthuxrequest.h new file mode 100644 index 000000000..b4a99539c --- /dev/null +++ b/src/core/api/qwebenginewebauthuxrequest.h @@ -0,0 +1,145 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWEBENGINEWEBAUTHUXREQUEST_H +#define QWEBENGINEWEBAUTHUXREQUEST_H + +#include <QtWebEngineCore/qtwebenginecoreglobal.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QWebEngineWebAuthUXRequestPrivate; +struct QWebEngineWebAuthPINRequest; + +class Q_WEBENGINECORE_EXPORT QWebEngineWebAuthUXRequest : public QObject +{ + Q_OBJECT +public: + QWebEngineWebAuthUXRequest(QWebEngineWebAuthUXRequestPrivate *); + ~QWebEngineWebAuthUXRequest(); + + enum WebAuthUXState { + NotStarted, + SelectAccount, + CollectPIN, + FinishTokenCollection, + RequestFailed, + Cancelled, + Completed + }; + Q_ENUM(WebAuthUXState) + + // The reason we are prompting for a new PIN. + enum class PINEntryReason : int { + // Indicates a new PIN is being set. + Set, + + // The existing PIN must be changed before using this authenticator. + Change, + + // The existing PIN is being collected to prove user verification. + Challenge + }; + Q_ENUM(PINEntryReason) + + // The errors that may prompt asking for a PIN. + enum class PINEntryError : int { + // No error has occurred. + NoError, + + // Internal UV is locked, so we are falling back to PIN. + InternalUvLocked, + + // The PIN the user entered does not match the authenticator PIN. + WrongPIN, + + // The new PIN the user entered is too short. + TooShort, + + // The new PIN the user entered contains invalid characters. + InvalidCharacters, + + // The new PIN the user entered is the same as the currently set PIN. + SameAsCurrentPIN, + }; + Q_ENUM(PINEntryError) + + enum class RequestFailureReason : int { + Timeout, + KeyNotRegistered, + KeyAlreadyRegistered, + SoftPINBlock, + HardPINBlock, + AuthenticatorRemovedDuringPINEntry, + AuthenticatorMissingResidentKeys, + AuthenticatorMissingUserVerification, + AuthenticatorMissingLargeBlob, + NoCommonAlgorithms, + // kStorageFull indicates that a resident credential could not be created + // because the authenticator has insufficient storage. + StorageFull, + UserConsentDenied, + // kWinUserCancelled means that the user clicked "Cancel" in the native + // Windows UI. + WinUserCancelled, + }; + Q_ENUM(RequestFailureReason) + + Q_PROPERTY(QStringList userNames READ userNames CONSTANT FINAL) + Q_PROPERTY(WebAuthUXState state READ state NOTIFY stateChanged FINAL) + Q_PROPERTY(QString relyingPartyId READ relyingPartyId CONSTANT FINAL) + Q_PROPERTY(QWebEngineWebAuthPINRequest pinRequest READ pinRequest CONSTANT FINAL) + Q_PROPERTY(RequestFailureReason requestFailureReason READ requestFailureReason CONSTANT FINAL) + + QStringList userNames() const; + QString relyingPartyId() const; + QWebEngineWebAuthPINRequest pinRequest() const; + WebAuthUXState state() const; + RequestFailureReason requestFailureReason() const; + +Q_SIGNALS: + void stateChanged(QWebEngineWebAuthUXRequest::WebAuthUXState state); + +public Q_SLOTS: + void cancel(); + void retry(); + void setSelectedAccount(const QString &selectedAccount); + void setPin(const QString &pin); + +private Q_SLOTS: + void handleUXUpdate(WebAuthUXState currentState); + +protected: + QScopedPointer<QWebEngineWebAuthUXRequestPrivate> d_ptr; + + Q_DECLARE_PRIVATE(QWebEngineWebAuthUXRequest) +}; + +struct Q_WEBENGINECORE_EXPORT QWebEngineWebAuthPINRequest +{ + Q_GADGET + + Q_PROPERTY(QWebEngineWebAuthUXRequest::PINEntryReason reason MEMBER reason CONSTANT FINAL) + Q_PROPERTY(QWebEngineWebAuthUXRequest::PINEntryError error MEMBER error CONSTANT FINAL) + Q_PROPERTY(qint32 minPinLength MEMBER minPinLength CONSTANT FINAL) + Q_PROPERTY(qint32 remainingAttempts MEMBER remainingAttempts CONSTANT FINAL) +public: + // Why this PIN is being collected. + QWebEngineWebAuthUXRequest::PINEntryReason reason; + + // The error for which we are prompting for a PIN. + QWebEngineWebAuthUXRequest::PINEntryError error = + QWebEngineWebAuthUXRequest::PINEntryError::NoError; + + // The minimum PIN length the authenticator will accept for the PIN. + qint32 minPinLength; + + // The number of attempts remaining before a hard lock. Should be ignored unless |mode| is + // kChallenge. + int remainingAttempts = 0; +}; + +QT_END_NAMESPACE + +#endif // QWEBENGINEWEBAUTHUXREQUEST_H diff --git a/src/core/api/qwebenginewebauthuxrequest_p.h b/src/core/api/qwebenginewebauthuxrequest_p.h new file mode 100644 index 000000000..45280bb0d --- /dev/null +++ b/src/core/api/qwebenginewebauthuxrequest_p.h @@ -0,0 +1,43 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QWEBENGINEWEBAUTHUXREQUEST_P_H +#define QWEBENGINEWEBAUTHUXREQUEST_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qtwebenginecoreglobal_p.h" +#include "qwebenginewebauthuxrequest.h" +#include <QtCore/QSharedPointer> + +namespace QtWebEngineCore { +class WebContentsAdapterClient; +class AuthenticatorRequestDialogController; +} + +QT_BEGIN_NAMESPACE + +class Q_WEBENGINECORE_PRIVATE_EXPORT QWebEngineWebAuthUXRequestPrivate +{ + +public: + QWebEngineWebAuthUXRequestPrivate( + QtWebEngineCore::AuthenticatorRequestDialogController *controller); + ~QWebEngineWebAuthUXRequestPrivate(); + + QWebEngineWebAuthUXRequest::WebAuthUXState m_currentState = + QWebEngineWebAuthUXRequest::NotStarted; + QtWebEngineCore::AuthenticatorRequestDialogController *webAuthDialogController; +}; + +QT_END_NAMESPACE +#endif // QWEBENGINEWEBAUTHUXREQUEST_P_H diff --git a/src/core/authenticator_request_client_delegate_qt.cpp b/src/core/authenticator_request_client_delegate_qt.cpp new file mode 100644 index 000000000..e999b74ab --- /dev/null +++ b/src/core/authenticator_request_client_delegate_qt.cpp @@ -0,0 +1,246 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "authenticator_request_client_delegate_qt.h" +#include "authenticator_request_dialog_controller.h" +#include "authenticator_request_dialog_controller_p.h" +#include "base/base64.h" +#include "profile_adapter_client.h" +#include "profile_qt.h" +#include "content/public/browser/web_contents.h" +#include "web_contents_delegate_qt.h" +#include "type_conversion.h" + +namespace QtWebEngineCore { + +using RequestFailureReason = QWebEngineWebAuthUXRequest::RequestFailureReason; + +WebAuthenticationDelegateQt::WebAuthenticationDelegateQt() = default; + +WebAuthenticationDelegateQt::~WebAuthenticationDelegateQt() = default; + +bool WebAuthenticationDelegateQt::SupportsResidentKeys(content::RenderFrameHost *render_frame_host) +{ + return true; +} + +AuthenticatorRequestClientDelegateQt::AuthenticatorRequestClientDelegateQt( + content::RenderFrameHost *render_frame_host) + : m_renderFrameHost(render_frame_host), m_weakFactory(this) +{ + m_dialogController.reset(new AuthenticatorRequestDialogController( + new AuthenticatorRequestDialogControllerPrivate(m_renderFrameHost, + m_weakFactory.GetWeakPtr()))); +} + +AuthenticatorRequestClientDelegateQt::~AuthenticatorRequestClientDelegateQt() +{ + // Currently WebAuth request is completed. Now it is possible to delete the dialog if displayed + m_dialogController->finishRequest(); +} + +void AuthenticatorRequestClientDelegateQt::SetRelyingPartyId(const std::string &rp_id) +{ + m_dialogController->setRelyingPartyId(rp_id); +} + +bool AuthenticatorRequestClientDelegateQt::DoesBlockRequestOnFailure( + InterestingFailureReason reason) +{ + if (!IsWebAuthnUIEnabled()) + return false; + + switch (reason) { + case InterestingFailureReason::kTimeout: + m_dialogController->handleRequestFailure(RequestFailureReason::Timeout); + break; + case InterestingFailureReason::kAuthenticatorMissingResidentKeys: + m_dialogController->handleRequestFailure( + RequestFailureReason::AuthenticatorMissingResidentKeys); + break; + case InterestingFailureReason::kAuthenticatorMissingUserVerification: + m_dialogController->handleRequestFailure( + RequestFailureReason::AuthenticatorMissingUserVerification); + break; + case InterestingFailureReason::kAuthenticatorMissingLargeBlob: + m_dialogController->handleRequestFailure( + RequestFailureReason::AuthenticatorMissingLargeBlob); + break; + case InterestingFailureReason::kAuthenticatorRemovedDuringPINEntry: + m_dialogController->handleRequestFailure( + RequestFailureReason::AuthenticatorRemovedDuringPINEntry); + break; + case InterestingFailureReason::kHardPINBlock: + m_dialogController->handleRequestFailure(RequestFailureReason::HardPINBlock); + break; + case InterestingFailureReason::kSoftPINBlock: + m_dialogController->handleRequestFailure(RequestFailureReason::SoftPINBlock); + break; + case InterestingFailureReason::kKeyAlreadyRegistered: + m_dialogController->handleRequestFailure(RequestFailureReason::KeyAlreadyRegistered); + break; + case InterestingFailureReason::kKeyNotRegistered: + m_dialogController->handleRequestFailure(RequestFailureReason::KeyNotRegistered); + break; + case InterestingFailureReason::kNoCommonAlgorithms: + m_dialogController->handleRequestFailure(RequestFailureReason::NoCommonAlgorithms); + break; + case InterestingFailureReason::kStorageFull: + m_dialogController->handleRequestFailure(RequestFailureReason::StorageFull); + break; + case InterestingFailureReason::kUserConsentDenied: + m_dialogController->handleRequestFailure(RequestFailureReason::UserConsentDenied); + break; + case InterestingFailureReason::kWinUserCancelled: +#if BUILDFLAG(IS_WIN) + m_dialogController->handleRequestFailure(RequestFailureReason::WinUserCancelled); +#else + return false; +#endif + break; + default: + return false; + } + return true; +} +void AuthenticatorRequestClientDelegateQt::RegisterActionCallbacks( + base::OnceClosure cancel_callback, base::RepeatingClosure start_over_callback, + AccountPreselectedCallback account_preselected_callback, + device::FidoRequestHandlerBase::RequestCallback request_callback, + base::RepeatingClosure bluetooth_adapter_power_on_callback) +{ + m_cancelCallback = std::move(cancel_callback); + m_startOverCallback = std::move(start_over_callback); + m_accountPreselectedCallback = std::move(account_preselected_callback); + m_requestCallback = std::move(request_callback); + m_bluetoothAdapterPowerOnCallback = std::move(bluetooth_adapter_power_on_callback); +} + +void AuthenticatorRequestClientDelegateQt::ShouldReturnAttestation( + const std::string &relying_party_id, const device::FidoAuthenticator *authenticator, + bool is_enterprise_attestation, base::OnceCallback<void(bool)> callback) +{ + std::move(callback).Run(!is_enterprise_attestation); +} + +void AuthenticatorRequestClientDelegateQt::SelectAccount( + std::vector<device::AuthenticatorGetAssertionResponse> responses, + base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> callback) +{ + if (m_isUiDisabled) { + // Requests with UI disabled should never reach account selection. + DCHECK(IsVirtualEnvironmentEnabled()); + std::move(callback).Run(std::move(responses.at(0))); + return; + } + + if (m_isConditionalRequest) { + return; + } + + m_authenticatorGetAssertionResponse = std::move(responses); + m_selectAccountCallback = std::move(callback); + + QStringList userList; + int nIndex = -1; + for (const auto &response : m_authenticatorGetAssertionResponse) { + nIndex++; + const auto &user_entity = response.user_entity; + const bool has_user_identifying_info = user_entity && user_entity->name; + if (has_user_identifying_info) { + QString userName = toQt(*response.user_entity->name); + m_userMap[userName] = nIndex; + userList.append(userName); + } + } + m_dialogController->selectAccount(userList); +} + +void AuthenticatorRequestClientDelegateQt::DisableUI() +{ + m_isUiDisabled = true; +} + +bool AuthenticatorRequestClientDelegateQt::IsWebAuthnUIEnabled() +{ + return !m_isUiDisabled; +} + +void AuthenticatorRequestClientDelegateQt::SetConditionalRequest(bool is_conditional) +{ + m_isConditionalRequest = is_conditional; +} + +// This method will not be invoked until the observer is set. +void AuthenticatorRequestClientDelegateQt::OnTransportAvailabilityEnumerated( + device::FidoRequestHandlerBase::TransportAvailabilityInfo data) +{ + // Show dialog only after this step; + // If m_isUiDisabled is set or another UI request in progress return + if (m_isUiDisabled || m_dialogController->state() != QWebEngineWebAuthUXRequest::NotStarted) + return; + + // Start WebAuth UX + // we may need to pass data as well. for SelectAccount and SupportPin it is not required, + // skipping that for the timebeing. + m_dialogController->startRequest(m_isConditionalRequest); +} + +bool AuthenticatorRequestClientDelegateQt::SupportsPIN() const +{ + return true; +} + +void AuthenticatorRequestClientDelegateQt::CollectPIN( + CollectPINOptions options, base::OnceCallback<void(std::u16string)> provide_pin_cb) +{ + + m_providePinCallback = std::move(provide_pin_cb); + QWebEngineWebAuthPINRequest pinRequestInfo; + + pinRequestInfo.reason = static_cast<QWebEngineWebAuthUXRequest::PINEntryReason>(options.reason); + pinRequestInfo.error = static_cast<QWebEngineWebAuthUXRequest::PINEntryError>(options.error); + pinRequestInfo.remainingAttempts = options.attempts; + pinRequestInfo.minPinLength = options.min_pin_length; + m_dialogController->collectPIN(pinRequestInfo); +} + +void AuthenticatorRequestClientDelegateQt::FinishCollectToken() +{ + m_dialogController->finishCollectToken(); +} + +void AuthenticatorRequestClientDelegateQt::onCancelRequest() +{ + if (!m_cancelCallback) + return; + + std::move(m_cancelCallback).Run(); +} + +void AuthenticatorRequestClientDelegateQt::onSelectAccount(const QString &selectedAccount) +{ + if (!m_selectAccountCallback) + return; + + if (m_userMap.find(selectedAccount) != m_userMap.end()) { + std::move(m_selectAccountCallback) + .Run(std::move(m_authenticatorGetAssertionResponse.at(m_userMap[selectedAccount]))); + } else { + onCancelRequest(); + } +} + +void AuthenticatorRequestClientDelegateQt::onCollectPin(const QString &pin) +{ + if (!m_providePinCallback) + return; + std::move(m_providePinCallback).Run(pin.toStdU16String()); +} + +void AuthenticatorRequestClientDelegateQt::onRetryRequest() +{ + DCHECK(m_startOverCallback); + m_startOverCallback.Run(); +} +} diff --git a/src/core/authenticator_request_client_delegate_qt.h b/src/core/authenticator_request_client_delegate_qt.h new file mode 100644 index 000000000..05c6136bd --- /dev/null +++ b/src/core/authenticator_request_client_delegate_qt.h @@ -0,0 +1,96 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef AUTHENTICATOR_REQUEST_CLIENT_DELEGATE_QT_H +#define AUTHENTICATOR_REQUEST_CLIENT_DELEGATE_QT_H + +#include "qtwebenginecoreglobal_p.h" +#include "content/public/browser/authenticator_request_client_delegate.h" +#include <unordered_map> +#include <QSharedPointer> + +namespace QtWebEngineCore { + +class AuthenticatorRequestDialogController; + +class WebAuthenticationDelegateQt : public content::WebAuthenticationDelegate +{ +public: + WebAuthenticationDelegateQt(); + virtual ~WebAuthenticationDelegateQt(); + + bool SupportsResidentKeys(content::RenderFrameHost *render_frame_host) override; +}; + +class AuthenticatorRequestClientDelegateQt : public content::AuthenticatorRequestClientDelegate +{ +public: + explicit AuthenticatorRequestClientDelegateQt(content::RenderFrameHost *render_frame_host); + AuthenticatorRequestClientDelegateQt(const AuthenticatorRequestClientDelegateQt &) = delete; + AuthenticatorRequestClientDelegateQt & + operator=(const AuthenticatorRequestClientDelegateQt &) = delete; + ~AuthenticatorRequestClientDelegateQt(); + + // content::AuthenticatorRequestClientDelegate ovverrides + void SetRelyingPartyId(const std::string &rp_id) override; + bool DoesBlockRequestOnFailure(InterestingFailureReason reason) override; + void RegisterActionCallbacks(base::OnceClosure cancel_callback, + base::RepeatingClosure start_over_callback, + AccountPreselectedCallback account_preselected_callback, + device::FidoRequestHandlerBase::RequestCallback request_callback, + base::RepeatingClosure bluetooth_adapter_power_on_callback) override; + void ShouldReturnAttestation(const std::string &relying_party_id, + const device::FidoAuthenticator *authenticator, + bool is_enterprise_attestation, + base::OnceCallback<void(bool)> callback) override; + void SelectAccount( + std::vector<device::AuthenticatorGetAssertionResponse> responses, + base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> callback) override; + void DisableUI() override; + bool IsWebAuthnUIEnabled() override; + void SetConditionalRequest(bool is_conditional) override; + + // device::FidoRequestHandlerBase::Observer overrides: + // This method will not be invoked until the observer is set. + void OnTransportAvailabilityEnumerated( + device::FidoRequestHandlerBase::TransportAvailabilityInfo data) override; + + bool SupportsPIN() const override; + void CollectPIN(CollectPINOptions options, + base::OnceCallback<void(std::u16string)> provide_pin_cb) override; + void FinishCollectToken() override; + + // Dialog helper + void onCancelRequest(); + void onSelectAccount(const QString &selectedAccount); + void onCollectPin(const QString &pin); + void onRetryRequest(); + +private: + content::RenderFrameHost *m_renderFrameHost; + bool m_isUiDisabled = false; + bool m_isConditionalRequest = false; + + base::OnceClosure m_cancelCallback; + base::RepeatingClosure m_startOverCallback; + AccountPreselectedCallback m_accountPreselectedCallback; + device::FidoRequestHandlerBase::RequestCallback m_requestCallback; + base::RepeatingClosure m_bluetoothAdapterPowerOnCallback; + + // Select account details; + std::vector<device::AuthenticatorGetAssertionResponse> m_authenticatorGetAssertionResponse; + std::unordered_map<QString, int> m_userMap; + base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)> m_selectAccountCallback; + + // collect pin + base::OnceCallback<void(std::u16string)> m_providePinCallback; + + // This member is used to keep authenticator request dialog controller alive until + // authenticator request is completed or cancelled. + QSharedPointer<AuthenticatorRequestDialogController> m_dialogController; + base::WeakPtrFactory<AuthenticatorRequestClientDelegateQt> m_weakFactory; +}; + +} + +#endif // AUTHENTICATOR_REQUEST_CLIENT_DELEGATE_QT_H diff --git a/src/core/authenticator_request_dialog_controller.cpp b/src/core/authenticator_request_dialog_controller.cpp new file mode 100644 index 000000000..f71aeaa39 --- /dev/null +++ b/src/core/authenticator_request_dialog_controller.cpp @@ -0,0 +1,301 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "authenticator_request_dialog_controller.h" +#include "authenticator_request_dialog_controller_p.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/browser_task_traits.h" +#include "web_contents_delegate_qt.h" +#include "qwebenginewebauthuxrequest_p.h" +#include "qwebenginewebauthuxrequest.h" + +using PINEntryError = QWebEngineWebAuthUXRequest::PINEntryError; +using PINEntryReason = QWebEngineWebAuthUXRequest::PINEntryReason; + +namespace QtWebEngineCore { + +ASSERT_ENUMS_MATCH(PINEntryReason::Set, device::pin::PINEntryReason::kSet) +ASSERT_ENUMS_MATCH(PINEntryReason::Change, device::pin::PINEntryReason::kChange) +ASSERT_ENUMS_MATCH(PINEntryReason::Challenge, device::pin::PINEntryReason::kChallenge) +ASSERT_ENUMS_MATCH(PINEntryError::WrongPIN, device::pin::PINEntryError::kWrongPIN) +ASSERT_ENUMS_MATCH(PINEntryError::TooShort, device::pin::PINEntryError::kTooShort) +ASSERT_ENUMS_MATCH(PINEntryError::SameAsCurrentPIN, device::pin::PINEntryError::kSameAsCurrentPIN) +ASSERT_ENUMS_MATCH(PINEntryError::NoError, device::pin::PINEntryError::kNoError) +ASSERT_ENUMS_MATCH(PINEntryError::InvalidCharacters, device::pin::PINEntryError::kInvalidCharacters) +ASSERT_ENUMS_MATCH(PINEntryError::InternalUvLocked, device::pin::PINEntryError::kInternalUvLocked) + +AuthenticatorRequestDialogControllerPrivate::AuthenticatorRequestDialogControllerPrivate( + content::RenderFrameHost *renderFrameHost, + base::WeakPtr<AuthenticatorRequestClientDelegateQt> authenticatorRequestDelegate) + : m_renderFrameHost(renderFrameHost) + , m_authenticatorRequestDelegate(authenticatorRequestDelegate) +{ +} + +AuthenticatorRequestDialogControllerPrivate::~AuthenticatorRequestDialogControllerPrivate() +{ + if (m_request) { + delete m_request; + m_request = nullptr; + } +} + +void AuthenticatorRequestDialogControllerPrivate::showWebAuthDialog() +{ + Q_ASSERT(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + + content::WebContents *webContent = content::WebContents::FromRenderFrameHost(m_renderFrameHost); + + if (!webContent) + return; + + WebContentsAdapterClient *adapterClient = nullptr; + if (webContent) + adapterClient = + static_cast<WebContentsDelegateQt *>(webContent->GetDelegate())->adapterClient(); + + if (adapterClient) { + + QWebEngineWebAuthUXRequestPrivate *itemPrivate = + new QWebEngineWebAuthUXRequestPrivate(q_ptr); + + m_request = new QWebEngineWebAuthUXRequest(itemPrivate); + + adapterClient->showWebAuthDialog(m_request); + m_isDialogCreated = true; + } else { + cancelRequest(); + } +} + +void AuthenticatorRequestDialogControllerPrivate::selectAccount(const QStringList &userList) +{ + m_userList.clear(); + m_userList = userList; + setCurrentState(QWebEngineWebAuthUXRequest::SelectAccount); +} + +void AuthenticatorRequestDialogControllerPrivate::collectPIN(QWebEngineWebAuthPINRequest pinRequest) +{ + m_pinRequest = pinRequest; + setCurrentState(QWebEngineWebAuthUXRequest::CollectPIN); +} + +void AuthenticatorRequestDialogControllerPrivate::finishCollectToken() +{ + setCurrentState(QWebEngineWebAuthUXRequest::FinishTokenCollection); +} + +QStringList AuthenticatorRequestDialogControllerPrivate::userNames() const +{ + return m_userList; +} + +void AuthenticatorRequestDialogControllerPrivate::finishRequest() +{ + if (!m_isDialogCreated) + return; + setCurrentState(QWebEngineWebAuthUXRequest::Completed); +} + +void AuthenticatorRequestDialogControllerPrivate::setCurrentState( + QWebEngineWebAuthUXRequest::WebAuthUXState uxState) +{ + if (!m_isStarted) { + // Dialog isn't showing yet. Remember to show this step when it appears. + m_pendingState = uxState; + return; + } + + m_currentState = uxState; + + if (m_isConditionalRequest) + return; + + if (!m_isDialogCreated) { + showWebAuthDialog(); + } else { + Q_EMIT q_ptr->stateChanged(m_currentState); + + if (m_currentState == QWebEngineWebAuthUXRequest::Cancelled + || m_currentState == QWebEngineWebAuthUXRequest::Completed) { + m_isDialogCreated = false; + } + } +} + +void AuthenticatorRequestDialogControllerPrivate::cancelRequest() +{ + setCurrentState(QWebEngineWebAuthUXRequest::Cancelled); + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&AuthenticatorRequestClientDelegateQt::onCancelRequest, + m_authenticatorRequestDelegate)); +} + +void AuthenticatorRequestDialogControllerPrivate::retryRequest() +{ + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&AuthenticatorRequestClientDelegateQt::onRetryRequest, + m_authenticatorRequestDelegate)); +} + +void AuthenticatorRequestDialogControllerPrivate::sendSelectAccountResponse( + const QString &selectedAccount) +{ + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&AuthenticatorRequestClientDelegateQt::onSelectAccount, + m_authenticatorRequestDelegate, selectedAccount)); +} + +QWebEngineWebAuthUXRequest::WebAuthUXState +AuthenticatorRequestDialogControllerPrivate::state() const +{ + return m_currentState; +} + +void AuthenticatorRequestDialogControllerPrivate::startRequest(bool isConditionalRequest) +{ + DCHECK(!m_isStarted); + + m_isStarted = true; + m_isConditionalRequest = isConditionalRequest; + + if (m_pendingState) { + setCurrentState(*m_pendingState); + m_pendingState.reset(); + } +} + +void AuthenticatorRequestDialogControllerPrivate::setRelyingPartyId(const QString &rpId) +{ + m_relyingPartyId = rpId; +} + +QString AuthenticatorRequestDialogControllerPrivate::relyingPartyId() const +{ + return m_relyingPartyId; +} + +QWebEngineWebAuthPINRequest AuthenticatorRequestDialogControllerPrivate::pinRequest() +{ + return m_pinRequest; +} + +void AuthenticatorRequestDialogControllerPrivate::handleRequestFailure( + QWebEngineWebAuthUXRequest::RequestFailureReason reason) +{ + m_requestFailureReason = reason; + setCurrentState(QWebEngineWebAuthUXRequest::RequestFailed); +} + +void AuthenticatorRequestDialogControllerPrivate::sendCollectPinResponse(const QString &pin) +{ + content::GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, + base::BindOnce(&AuthenticatorRequestClientDelegateQt::onCollectPin, + m_authenticatorRequestDelegate, pin)); +} + +QWebEngineWebAuthUXRequest::RequestFailureReason +AuthenticatorRequestDialogControllerPrivate::requestFailureReason() const +{ + return m_requestFailureReason; +} + +AuthenticatorRequestDialogController::AuthenticatorRequestDialogController( + AuthenticatorRequestDialogControllerPrivate *dd) +{ + Q_ASSERT(dd); + d_ptr.reset(dd); + d_ptr->q_ptr = this; +} + +AuthenticatorRequestDialogController::~AuthenticatorRequestDialogController() { } + +void AuthenticatorRequestDialogController::selectAccount(const QStringList &userList) +{ + d_ptr->selectAccount(userList); +} + +void AuthenticatorRequestDialogController::collectPIN(QWebEngineWebAuthPINRequest pinRequest) +{ + d_ptr->collectPIN(pinRequest); +} + +QStringList AuthenticatorRequestDialogController::userNames() const +{ + return d_ptr->userNames(); +} + +QWebEngineWebAuthPINRequest AuthenticatorRequestDialogController::pinRequest() +{ + return d_ptr->pinRequest(); +} + +void AuthenticatorRequestDialogController::reject() +{ + d_ptr->cancelRequest(); +} + +void AuthenticatorRequestDialogController::sendSelectAccountResponse(const QString &account) +{ + d_ptr->sendSelectAccountResponse(account); +} + +void AuthenticatorRequestDialogController::finishCollectToken() +{ + d_ptr->finishCollectToken(); +} + +void AuthenticatorRequestDialogController::finishRequest() +{ + d_ptr->finishRequest(); +} + +QWebEngineWebAuthUXRequest::WebAuthUXState AuthenticatorRequestDialogController::state() const +{ + return d_ptr->state(); +} + +void AuthenticatorRequestDialogController::startRequest(bool bIsConditionalRequest) +{ + d_ptr->startRequest(bIsConditionalRequest); +} + +void AuthenticatorRequestDialogController::setRelyingPartyId(const std::string &rpId) +{ + d_ptr->setRelyingPartyId(QString::fromStdString(rpId)); +} + +QString AuthenticatorRequestDialogController::relyingPartyId() const +{ + return d_ptr->relyingPartyId(); +} + +void AuthenticatorRequestDialogController::handleRequestFailure( + QWebEngineWebAuthUXRequest::RequestFailureReason reason) +{ + d_ptr->handleRequestFailure(reason); +} + +void AuthenticatorRequestDialogController::retryRequest() +{ + d_ptr->retryRequest(); +} + +void AuthenticatorRequestDialogController::sendCollectPinResponse(const QString &pin) +{ + d_ptr->sendCollectPinResponse(pin); +} + +QWebEngineWebAuthUXRequest::RequestFailureReason +AuthenticatorRequestDialogController::requestFailureReason() const +{ + return d_ptr->requestFailureReason(); +} +} diff --git a/src/core/authenticator_request_dialog_controller.h b/src/core/authenticator_request_dialog_controller.h new file mode 100644 index 000000000..6d81fc80d --- /dev/null +++ b/src/core/authenticator_request_dialog_controller.h @@ -0,0 +1,56 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H +#define AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H + +#include <QtWebEngineCore/private/qtwebenginecoreglobal_p.h> +#include <QtCore/qobject.h> +#include "qwebenginewebauthuxrequest.h" + +// #include "base/functional/callback_forward.h" +// #include "device/fido/authenticator_get_assertion_response.h" + +namespace content { +class WebContents; +class RenderFrameHost; +} +namespace QtWebEngineCore { + +class AuthenticatorRequestDialogControllerPrivate; + +class Q_WEBENGINECORE_EXPORT AuthenticatorRequestDialogController : public QObject +{ + Q_OBJECT +public: + ~AuthenticatorRequestDialogController(); + void sendSelectAccountResponse(const QString &account); + void sendCollectPinResponse(const QString &pin); + QStringList userNames() const; + QWebEngineWebAuthPINRequest pinRequest(); + void reject(); + AuthenticatorRequestDialogController(AuthenticatorRequestDialogControllerPrivate *); + + QWebEngineWebAuthUXRequest::WebAuthUXState state() const; + QString relyingPartyId() const; + void retryRequest(); + QWebEngineWebAuthUXRequest::RequestFailureReason requestFailureReason() const; + +Q_SIGNALS: + void stateChanged(QWebEngineWebAuthUXRequest::WebAuthUXState state); + +private: + void selectAccount(const QStringList &userList); + void collectPIN(QWebEngineWebAuthPINRequest pinRequest); + void finishCollectToken(); + void startRequest(bool bIsConditionalRequest); + void finishRequest(); + void setRelyingPartyId(const std::string &rpId); + void handleRequestFailure(QWebEngineWebAuthUXRequest::RequestFailureReason reason); + + QScopedPointer<AuthenticatorRequestDialogControllerPrivate> d_ptr; + friend class AuthenticatorRequestClientDelegateQt; +}; +} + +#endif // AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H diff --git a/src/core/authenticator_request_dialog_controller_p.h b/src/core/authenticator_request_dialog_controller_p.h new file mode 100644 index 000000000..abb2710bf --- /dev/null +++ b/src/core/authenticator_request_dialog_controller_p.h @@ -0,0 +1,79 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_P_H +#define AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_P_H +#include <QStringList> +#include <QSharedPointer> +#include "authenticator_request_client_delegate_qt.h" +#include "authenticator_request_dialog_controller.h" + +namespace content { +class WebContents; +class RenderFrameHost; +} + +namespace QtWebEngineCore { + +class AuthenticatorRequestDialogControllerPrivate +{ + +public: + AuthenticatorRequestDialogControllerPrivate( + content::RenderFrameHost *renderFrameHost, + base::WeakPtr<AuthenticatorRequestClientDelegateQt> authenticatorRequestDelegate); + ~AuthenticatorRequestDialogControllerPrivate(); + void showWebAuthDialog(); + void selectAccount(const QStringList &userList); + QStringList userNames() const; + QString relyingPartyId() const; + QWebEngineWebAuthUXRequest::WebAuthUXState state() const; + QWebEngineWebAuthPINRequest pinRequest(); + QWebEngineWebAuthUXRequest::RequestFailureReason requestFailureReason() const; + void sendSelectAccountResponse(const QString &selectedAccount); + void setCurrentState(QWebEngineWebAuthUXRequest::WebAuthUXState uxState); + void setRelyingPartyId(const QString &rpId); + + // Support pin functionality + void collectPIN(QWebEngineWebAuthPINRequest pinRequestInfo); + void finishCollectToken(); + void handleRequestFailure(QWebEngineWebAuthUXRequest::RequestFailureReason reason); + void sendCollectPinResponse(const QString &pin); + + // Deleting dialog; + void finishRequest(); + + // cancel request + void cancelRequest(); + void retryRequest(); + void startRequest(bool isConditionalRequest); + + AuthenticatorRequestDialogController *q_ptr; + +private: + content::RenderFrameHost *m_renderFrameHost; + QStringList m_userList; + // QString m_selectedAccount; + QString m_pin; + QString m_relyingPartyId; + + bool m_isStarted = false; + bool m_isConditionalRequest = false; + QWebEngineWebAuthUXRequest::WebAuthUXState m_currentState = + QWebEngineWebAuthUXRequest::NotStarted; + base::WeakPtr<AuthenticatorRequestClientDelegateQt> m_authenticatorRequestDelegate; + bool m_isDialogCreated = false; + QWebEngineWebAuthPINRequest m_pinRequest; + + QWebEngineWebAuthUXRequest *m_request; + QWebEngineWebAuthUXRequest::RequestFailureReason m_requestFailureReason; + + // m_pendingState holds requested steps until the UI is shown. The UI is only + // shown once the TransportAvailabilityInfo is available, but authenticators + // may request, e.g., PIN entry prior to that. + absl::optional<QWebEngineWebAuthUXRequest::WebAuthUXState> m_pendingState; +}; + +} // namespace QtWebEngineCore + +#endif // AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_P_H diff --git a/src/core/content_browser_client_qt.cpp b/src/core/content_browser_client_qt.cpp index bf39ebe2f..904ed87a5 100644 --- a/src/core/content_browser_client_qt.cpp +++ b/src/core/content_browser_client_qt.cpp @@ -83,6 +83,7 @@ #include "web_engine_context.h" #include "web_engine_library_info.h" #include "web_engine_settings.h" +#include "authenticator_request_client_delegate_qt.h" #include "api/qwebenginecookiestore.h" #include "api/qwebenginecookiestore_p.h" #include "api/qwebengineurlrequestinfo_p.h" @@ -1350,4 +1351,19 @@ ContentBrowserClientQt::AllowWebBluetooth(content::BrowserContext *browser_conte return content::ContentBrowserClient::AllowWebBluetoothResult::BLOCK_GLOBALLY_DISABLED; } +content::WebAuthenticationDelegate *ContentBrowserClientQt::GetWebAuthenticationDelegate() +{ + static base::NoDestructor<WebAuthenticationDelegateQt> delegate; + return delegate.get(); +} + +#if !BUILDFLAG(IS_ANDROID) +std::unique_ptr<content::AuthenticatorRequestClientDelegate> +ContentBrowserClientQt::GetWebAuthenticationRequestDelegate( + content::RenderFrameHost *render_frame_host) +{ + return std::make_unique<AuthenticatorRequestClientDelegateQt>(render_frame_host); +} +#endif + } // namespace QtWebEngineCore diff --git a/src/core/content_browser_client_qt.h b/src/core/content_browser_client_qt.h index 87e48db5a..e93a3f117 100644 --- a/src/core/content_browser_client_qt.h +++ b/src/core/content_browser_client_qt.h @@ -233,6 +233,12 @@ public: blink::UserAgentMetadata GetUserAgentMetadata() override { return getUserAgentMetadata(); } std::string GetProduct() override; + content::WebAuthenticationDelegate *GetWebAuthenticationDelegate() override; +#if !BUILDFLAG(IS_ANDROID) + std::unique_ptr<content::AuthenticatorRequestClientDelegate> + GetWebAuthenticationRequestDelegate(content::RenderFrameHost *render_frame_host) override; +#endif + private: BrowserMainPartsQt *m_browserMainParts = nullptr; }; diff --git a/src/core/doc/src/qwebenginepage_lgpl.qdoc b/src/core/doc/src/qwebenginepage_lgpl.qdoc index fe84ff76b..93c46e417 100644 --- a/src/core/doc/src/qwebenginepage_lgpl.qdoc +++ b/src/core/doc/src/qwebenginepage_lgpl.qdoc @@ -816,3 +816,15 @@ \sa url() */ + +/*! + \fn void QWebEnginePage::webAuthUXRequested(QWebEngineWebAuthUXRequest *request); + \since 6.7 + + This signal is emitted when a WebAuth authenticator needs user interaction + during the authentication process. These requests are handled by displaying a dialog to the user. + + The \a request contains the information and API required to complete the WebAuth UX request. + + \sa QWebEngineWebAuthUXRequest +*/ diff --git a/src/core/web_contents_adapter_client.h b/src/core/web_contents_adapter_client.h index afbd2b5f2..4a6b1020c 100644 --- a/src/core/web_contents_adapter_client.h +++ b/src/core/web_contents_adapter_client.h @@ -36,6 +36,7 @@ QT_FORWARD_DECLARE_CLASS(QWebEngineUrlRequestInterceptor) QT_FORWARD_DECLARE_CLASS(QWebEngineContextMenuRequest) QT_FORWARD_DECLARE_CLASS(QWebEngineCertificateError) QT_FORWARD_DECLARE_CLASS(QWebEngineSettings) +QT_FORWARD_DECLARE_CLASS(QWebEngineWebAuthUXRequest) namespace content { struct DropData; @@ -215,6 +216,7 @@ public: virtual ProfileAdapter *profileAdapter() = 0; virtual WebContentsAdapter* webContentsAdapter() = 0; virtual void releaseProfile() = 0; + virtual void showWebAuthDialog(QWebEngineWebAuthUXRequest *request) = 0; }; } // namespace QtWebEngineCore diff --git a/src/webenginequick/api/qquickwebengineforeigntypes_p.h b/src/webenginequick/api/qquickwebengineforeigntypes_p.h index 1591e596f..7acad61e4 100644 --- a/src/webenginequick/api/qquickwebengineforeigntypes_p.h +++ b/src/webenginequick/api/qquickwebengineforeigntypes_p.h @@ -31,6 +31,7 @@ #include <QtWebEngineCore/qwebenginecontextmenurequest.h> #include <QtWebEngineCore/qwebengineregisterprotocolhandlerrequest.h> #include <QtWebEngineCore/qwebenginefilesystemaccessrequest.h> +#include <QtWebEngineCore/qwebenginewebauthuxrequest.h> QT_BEGIN_NAMESPACE @@ -198,6 +199,13 @@ namespace ForeginWebEngineFileSystemAccessRequestNamespace QML_ADDED_IN_VERSION(6, 4) }; +namespace ForeignWebEngineWebAuthUXRequest { + Q_NAMESPACE + QML_FOREIGN_NAMESPACE(QWebEngineWebAuthUXRequest) + QML_NAMED_ELEMENT(WebEngineWebAuthUXRequest) + QML_ADDED_IN_VERSION(6, 7) +}; + QT_END_NAMESPACE #endif // QQUICKWEBENGINEFOREIGNTYPES_H diff --git a/src/webenginequick/api/qquickwebengineview.cpp b/src/webenginequick/api/qquickwebengineview.cpp index aafec7295..882b59b6b 100644 --- a/src/webenginequick/api/qquickwebengineview.cpp +++ b/src/webenginequick/api/qquickwebengineview.cpp @@ -39,6 +39,7 @@ #include <QtWebEngineCore/qwebenginepage.h> #include <QtWebEngineCore/qwebengineregisterprotocolhandlerrequest.h> #include <QtWebEngineCore/qwebenginescriptcollection.h> +#include <QtWebEngineCore/qwebenginewebauthuxrequest.h> #include <QtWebEngineCore/private/qwebenginecontextmenurequest_p.h> #include <QtWebEngineCore/private/qwebenginehistory_p.h> #include <QtWebEngineCore/private/qwebenginenewwindowrequest_p.h> @@ -1308,6 +1309,12 @@ void QQuickWebEngineViewPrivate::hideTouchSelectionMenu() ui()->hideTouchSelectionMenu(); } +void QQuickWebEngineViewPrivate::showWebAuthDialog(QWebEngineWebAuthUXRequest *request) +{ + Q_Q(QQuickWebEngineView); + Q_EMIT q->webAuthUXRequested(request); +} + bool QQuickWebEngineView::isLoading() const { Q_D(const QQuickWebEngineView); diff --git a/src/webenginequick/api/qquickwebengineview_p.h b/src/webenginequick/api/qquickwebengineview_p.h index 7084eb173..63cf2c2f4 100644 --- a/src/webenginequick/api/qquickwebengineview_p.h +++ b/src/webenginequick/api/qquickwebengineview_p.h @@ -51,6 +51,7 @@ class QWebEngineNewWindowRequest; class QWebEngineRegisterProtocolHandlerRequest; class QQuickWebEngineScriptCollection; class QQuickWebEngineTouchSelectionMenuRequest; +class QWebEngineWebAuthUXRequest; class Q_WEBENGINEQUICK_PRIVATE_EXPORT QQuickWebEngineView : public QQuickItem { Q_OBJECT @@ -548,6 +549,7 @@ Q_SIGNALS: Q_REVISION(6,3) void touchSelectionMenuRequested(QQuickWebEngineTouchSelectionMenuRequest *request); Q_REVISION(6,4) void touchHandleDelegateChanged(); Q_REVISION(6,4) void fileSystemAccessRequested(const QWebEngineFileSystemAccessRequest &request); + Q_REVISION(6, 7) void webAuthUXRequested(QWebEngineWebAuthUXRequest *request); protected: void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; diff --git a/src/webenginequick/api/qquickwebengineview_p_p.h b/src/webenginequick/api/qquickwebengineview_p_p.h index aa67fd291..352d4a8c0 100644 --- a/src/webenginequick/api/qquickwebengineview_p_p.h +++ b/src/webenginequick/api/qquickwebengineview_p_p.h @@ -132,6 +132,7 @@ public: void showAutofillPopup(QtWebEngineCore::AutofillPopupController *controller, const QRect &bounds, bool autoselectFirstSuggestion) override; void hideAutofillPopup() override; + void showWebAuthDialog(QWebEngineWebAuthUXRequest *request) override; void updateAction(QQuickWebEngineView::WebAction) const; bool adoptWebContents(QtWebEngineCore::WebContentsAdapter *webContents); diff --git a/src/webenginequick/doc/src/webengineview_lgpl.qdoc b/src/webenginequick/doc/src/webengineview_lgpl.qdoc index c4d87dc4d..45b9c3c79 100644 --- a/src/webenginequick/doc/src/webengineview_lgpl.qdoc +++ b/src/webenginequick/doc/src/webengineview_lgpl.qdoc @@ -1562,5 +1562,17 @@ \sa QWebEngineDownloadRequest::SavePageFormat */ +/*! + \qmlsignal WebEngineView::webAuthUXRequested(QWebEngineWebAuthUXRequest *request); + \since QtWebEngine 6.7 + + This signal is emitted when a WebAuth authenticator requires user interaction + during the authentication process. These requests are handled by displaying a dialog to the user. + + The \a request contains the information and API required to complete the WebAuth UX request. + + \sa QWebEngineWebAuthUXRequest +*/ + \sa {WebEngine Qt Quick Custom Touch Handle Example} */ diff --git a/tests/auto/quick/publicapi/tst_publicapi.cpp b/tests/auto/quick/publicapi/tst_publicapi.cpp index 2c85b5d25..5f9d262a7 100644 --- a/tests/auto/quick/publicapi/tst_publicapi.cpp +++ b/tests/auto/quick/publicapi/tst_publicapi.cpp @@ -23,6 +23,7 @@ #include <QtWebEngineCore/QWebEngineDownloadRequest> #include <QtWebEngineCore/QWebEngineScript> #include <QtWebEngineCore/QWebEngineLoadingInfo> +#include <QtWebEngineCore/QWebEngineWebAuthUXRequest> #include <private/qquickwebengineview_p.h> #include <private/qquickwebengineaction_p.h> #include <private/qquickwebengineclientcertificateselection_p.h> @@ -70,6 +71,8 @@ static const QList<const QMetaObject *> typesToCheck = QList<const QMetaObject * << &QWebEngineQuotaRequest::staticMetaObject << &QWebEngineRegisterProtocolHandlerRequest::staticMetaObject << &QQuickWebEngineTouchSelectionMenuRequest::staticMetaObject + << &QWebEngineWebAuthUXRequest::staticMetaObject + << &QWebEngineWebAuthPINRequest::staticMetaObject ; static QList<QMetaEnum> knownEnumNames = QList<QMetaEnum>() @@ -813,6 +816,50 @@ static const QStringList expectedAPI = QStringList() << "QWebEngineNotification.click() --> void" << "QWebEngineNotification.close() --> void" << "QWebEngineNotification.closed() --> void" + << "QQuickWebEngineView.webAuthUXRequested(QWebEngineWebAuthUXRequest*) --> void" + << "QWebEngineWebAuthUXRequest.NotStarted --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.SelectAccount --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.CollectPIN --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.FinishTokenCollection --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.RequestFailed --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.Cancelled --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.Completed --> WebAuthUXState" + << "QWebEngineWebAuthUXRequest.PINEntryReason.Set --> PINEntryReason" + << "QWebEngineWebAuthUXRequest.PINEntryReason.Change --> PINEntryReason" + << "QWebEngineWebAuthUXRequest.PINEntryReason.Challenge --> PINEntryReason" + << "QWebEngineWebAuthUXRequest.PINEntryError.NoError --> PINEntryError" + << "QWebEngineWebAuthUXRequest.PINEntryError.InternalUvLocked --> PINEntryError" + << "QWebEngineWebAuthUXRequest.PINEntryError.WrongPIN --> PINEntryError" + << "QWebEngineWebAuthUXRequest.PINEntryError.TooShort --> PINEntryError" + << "QWebEngineWebAuthUXRequest.PINEntryError.InvalidCharacters --> PINEntryError" + << "QWebEngineWebAuthUXRequest.PINEntryError.SameAsCurrentPIN --> PINEntryError" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.Timeout --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.KeyNotRegistered --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.KeyAlreadyRegistered --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.SoftPINBlock --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.HardPINBlock --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.AuthenticatorRemovedDuringPINEntry --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.AuthenticatorMissingResidentKeys --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.AuthenticatorMissingUserVerification --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.AuthenticatorMissingLargeBlob --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.NoCommonAlgorithms --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.StorageFull --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.UserConsentDenied --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.RequestFailureReason.WinUserCancelled --> RequestFailureReason" + << "QWebEngineWebAuthUXRequest.userNames --> QStringList" + << "QWebEngineWebAuthUXRequest.state --> QWebEngineWebAuthUXRequest::WebAuthUXState" + << "QWebEngineWebAuthUXRequest.relyingPartyId --> QString" + << "QWebEngineWebAuthUXRequest.pinRequest --> QWebEngineWebAuthPINRequest" + << "QWebEngineWebAuthUXRequest.requestFailureReason --> QWebEngineWebAuthUXRequest::RequestFailureReason" + << "QWebEngineWebAuthUXRequest.stateChanged(QWebEngineWebAuthUXRequest::WebAuthUXState) --> void" + << "QWebEngineWebAuthUXRequest.cancel() --> void" + << "QWebEngineWebAuthUXRequest.retry() --> void" + << "QWebEngineWebAuthUXRequest.setSelectedAccount(QString) --> void" + << "QWebEngineWebAuthUXRequest.setPin(QString) --> void" + << "QWebEngineWebAuthPINRequest.reason --> QWebEngineWebAuthUXRequest::PINEntryReason" + << "QWebEngineWebAuthPINRequest.error --> QWebEngineWebAuthUXRequest::PINEntryError" + << "QWebEngineWebAuthPINRequest.minPinLength --> int" + << "QWebEngineWebAuthPINRequest.remainingAttempts --> int" ; static bool isCheckedEnum(QMetaType t) |