Reland "webauthn: add a basic implementation of conditional create requests"
This is a reland of commit 34be495626
Original change's description:
> webauthn: add a basic implementation of conditional create requests
>
> For WebAuthn create() requests with mediation='conditional', the browser
> can create a credential and resolve the request promise without any user
> interaction, if the user has previously agreed to create credentials and
> the browser has recently mediated an authentication.
>
> In Chrome's implementation, we create a credential in GPM if the
> following conditions are true:
> - The user has a matching password in GPM for the same username that can
> be filled on the site making the WebAuthn request.
> - The password has recently been used.
>
> To handle conditional create requests, this change adds a new request
> controller class that inherits from DocumentUserData. The request
> controller is responsible for determining whether a matching password
> exists, driving interaction with the enclave, and showing post-request
> confirmation UI (not yet implemented).
>
> The GPMEnclaveController, which usually drives enclave interactions for
> WebAuthn requests, is not instantiated for conditional create requests,
> since it is tightly coupled with the modal UI.
>
> The implementation is gated on the default-disabled
> `kWebAuthnGpmPasskeyUpgrade` feature flag.
>
> Blink Intent To Prototype: https://groups.google.com/a/chromium.org/g/blink-dev/c/XFJmqtQpMds
>
> Change-Id: I723ef3115cd00e39df721443a64df46fc0f38aea
> Bug: 377758786
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6013246
> Reviewed-by: Kent Tamura <tkent@chromium.org>
> Reviewed-by: Adam Langley <agl@chromium.org>
> Reviewed-by: Ken Buchanan <kenrb@chromium.org>
> Commit-Queue: Martin Kreichgauer <martinkr@google.com>
> Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1382464}
Bug: 377758786
Change-Id: Idea623fdb380b580159e2a92a881efe519efafcb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6033512
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Auto-Submit: Martin Kreichgauer <martinkr@google.com>
Reviewed-by: Kent Tamura <tkent@chromium.org>
Reviewed-by: Adam Langley <agl@chromium.org>
Reviewed-by: Ken Buchanan <kenrb@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1385260}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
39497a60f0
commit
980dcfb104
chrome/browser
components/webauthn/json
content
browser
child
public
device/fido
third_party/blink
public
mojom
webauthn
renderer
modules
credentialmanagement
platform
tools/metrics/histograms/metadata/webauthn
@ -4942,6 +4942,8 @@ static_library("ui") {
|
||||
"webauthn/passkey_saved_confirmation_controller.h",
|
||||
"webauthn/passkey_updated_confirmation_controller.cc",
|
||||
"webauthn/passkey_updated_confirmation_controller.h",
|
||||
"webauthn/passkey_upgrade_request_controller.cc",
|
||||
"webauthn/passkey_upgrade_request_controller.h",
|
||||
"webauthn/sheet_models.cc",
|
||||
"webauthn/sheet_models.h",
|
||||
"webauthn/transport_hover_list_model.cc",
|
||||
|
@ -410,6 +410,7 @@ std::unique_ptr<AuthenticatorRequestSheetView> CreateSheetViewForCurrentStepOf(
|
||||
break;
|
||||
case Step::kNotStarted:
|
||||
case Step::kPasskeyAutofill:
|
||||
case Step::kPasskeyUpgrade:
|
||||
case Step::kClosed:
|
||||
case Step::kRecoverSecurityDomain:
|
||||
case Step::kGPMReauthForPinReset:
|
||||
|
172
chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc
Normal file
172
chrome/browser/ui/webauthn/passkey_upgrade_request_controller.cc
Normal file
@ -0,0 +1,172 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "chrome/browser/ui/webauthn/passkey_upgrade_request_controller.h"
|
||||
|
||||
#include "chrome/browser/password_manager/account_password_store_factory.h"
|
||||
#include "chrome/browser/password_manager/profile_password_store_factory.h"
|
||||
#include "chrome/browser/profiles/profile.h"
|
||||
#include "chrome/browser/webauthn/enclave_manager_factory.h"
|
||||
#include "chrome/browser/webauthn/gpm_enclave_controller.h"
|
||||
#include "chrome/browser/webauthn/passkey_model_factory.h"
|
||||
#include "components/device_event_log/device_event_log.h"
|
||||
#include "components/password_manager/core/browser/form_parsing/form_data_parser.h"
|
||||
#include "components/password_manager/core/browser/password_store/password_store.h"
|
||||
#include "components/password_manager/core/browser/password_store/password_store_util.h"
|
||||
#include "content/public/browser/browser_context.h"
|
||||
#include "content/public/browser/document_user_data.h"
|
||||
#include "content/public/browser/render_frame_host.h"
|
||||
#include "device/fido/fido_discovery_factory.h"
|
||||
|
||||
using RenderFrameHost = content::RenderFrameHost;
|
||||
|
||||
DOCUMENT_USER_DATA_KEY_IMPL(PasskeyUpgradeRequestController);
|
||||
|
||||
PasskeyUpgradeRequestController::~PasskeyUpgradeRequestController() = default;
|
||||
|
||||
void PasskeyUpgradeRequestController::InitializeEnclaveRequestCallback(
|
||||
device::FidoDiscoveryFactory* discovery_factory) {
|
||||
using EnclaveEventStream = device::FidoDiscoveryBase::EventStream<
|
||||
std::unique_ptr<device::enclave::CredentialRequest>>;
|
||||
std::unique_ptr<EnclaveEventStream> event_stream;
|
||||
std::tie(enclave_request_callback_, event_stream) = EnclaveEventStream::New();
|
||||
discovery_factory->set_enclave_ui_request_stream(std::move(event_stream));
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::TryUpgradePasswordToPasskey(
|
||||
std::string rp_id,
|
||||
const std::string& user_name,
|
||||
base::OnceCallback<void(bool success)> callback) {
|
||||
CHECK(enclave_request_callback_);
|
||||
CHECK(!pending_callback_);
|
||||
pending_callback_ = std::move(callback);
|
||||
rp_id_ = std::move(rp_id);
|
||||
user_name_ = base::UTF8ToUTF16(user_name);
|
||||
|
||||
switch (enclave_state_) {
|
||||
case EnclaveState::kUnknown:
|
||||
// EnclaveLoaded() will invoke ContinuePendingUpgradeRequest().
|
||||
pending_upgrade_request_ = true;
|
||||
break;
|
||||
case EnclaveState::kNotReady:
|
||||
FIDO_LOG(EVENT) << "Passkey upgrade request failed because the enclave "
|
||||
"isn't initialized.";
|
||||
std::move(pending_callback_).Run(false);
|
||||
break;
|
||||
case EnclaveState::kReady:
|
||||
pending_upgrade_request_ = true;
|
||||
ContinuePendingUpgradeRequest();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::ContinuePendingUpgradeRequest() {
|
||||
CHECK_EQ(enclave_state_, EnclaveState::kReady);
|
||||
CHECK(pending_upgrade_request_);
|
||||
pending_upgrade_request_ = false;
|
||||
|
||||
// TODO(crbug.com/377758786): The profile password store is probably wrong in
|
||||
// some cases. Find out how to query GPM specifically.
|
||||
scoped_refptr<password_manager::PasswordStoreInterface> password_store =
|
||||
ProfilePasswordStoreFactory::GetForProfile(
|
||||
profile(), ServiceAccessType::EXPLICIT_ACCESS);
|
||||
GURL url = origin().GetURL();
|
||||
password_manager::PasswordFormDigest form_digest(
|
||||
password_manager::PasswordForm::Scheme::kHtml,
|
||||
password_manager::GetSignonRealm(url), url);
|
||||
password_store->GetLogins(form_digest, weak_factory_.GetWeakPtr());
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::OnGetPasswordStoreResultsOrErrorFrom(
|
||||
password_manager::PasswordStoreInterface* store,
|
||||
password_manager::LoginsResultOrError results_or_error) {
|
||||
if (absl::holds_alternative<password_manager::PasswordStoreBackendError>(
|
||||
results_or_error)) {
|
||||
FIDO_LOG(EVENT) << "Passkey upgrade failed due to password store error";
|
||||
}
|
||||
CHECK(pending_callback_);
|
||||
password_manager::LoginsResult result =
|
||||
password_manager::GetLoginsOrEmptyListOnFailure(results_or_error);
|
||||
bool found = false;
|
||||
// Passwords must have been used within the last 90 days in order to be
|
||||
// eligible.
|
||||
const auto min_last_used = base::Time::Now() - base::Days(90);
|
||||
for (const password_manager::PasswordForm& password_form : result) {
|
||||
if (password_form.username_value == user_name_ &&
|
||||
password_form.date_last_used >= min_last_used) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
std::move(pending_callback_).Run(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CHECK(enclave_request_callback_);
|
||||
// TODO(crbug.com/377758786): Make the request up=0.
|
||||
enclave_transaction_ = std::make_unique<GPMEnclaveTransaction>(
|
||||
/*delegate=*/this, PasskeyModelFactory::GetForProfile(profile()),
|
||||
device::FidoRequestType::kMakeCredential, rp_id_,
|
||||
EnclaveUserVerificationMethod::kNone,
|
||||
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(profile()),
|
||||
/*pin=*/std::nullopt, /*selected_credential_id=*/std::nullopt,
|
||||
enclave_request_callback_);
|
||||
enclave_transaction_->Start();
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::HandleEnclaveTransactionError() {
|
||||
if (!pending_callback_) {
|
||||
return;
|
||||
}
|
||||
FIDO_LOG(ERROR) << "Passkey upgrade failed on enclave error";
|
||||
std::move(pending_callback_).Run(false);
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::BuildUVKeyOptions(
|
||||
EnclaveManager::UVKeyOptions&) {
|
||||
// Upgrade requests don't perform user verification.
|
||||
NOTIMPLEMENTED();
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::HandlePINValidationResult(
|
||||
device::enclave::PINValidationResult) {
|
||||
// Upgrade requests don't perform user verification.
|
||||
NOTIMPLEMENTED();
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::OnPasskeyCreated(
|
||||
const sync_pb::WebauthnCredentialSpecifics& passkey) {
|
||||
CHECK(pending_callback_);
|
||||
std::move(pending_callback_).Run(true);
|
||||
}
|
||||
|
||||
Profile* PasskeyUpgradeRequestController::profile() const {
|
||||
return Profile::FromBrowserContext(render_frame_host().GetBrowserContext());
|
||||
}
|
||||
|
||||
void PasskeyUpgradeRequestController::OnEnclaveLoaded() {
|
||||
CHECK(enclave_manager_->is_loaded());
|
||||
CHECK_EQ(enclave_state_, EnclaveState::kUnknown);
|
||||
enclave_state_ = enclave_manager_->is_ready() ? EnclaveState::kReady
|
||||
: EnclaveState::kNotReady;
|
||||
if (pending_upgrade_request_) {
|
||||
ContinuePendingUpgradeRequest();
|
||||
}
|
||||
}
|
||||
|
||||
PasskeyUpgradeRequestController::PasskeyUpgradeRequestController(
|
||||
RenderFrameHost* rfh)
|
||||
: content::DocumentUserData<PasskeyUpgradeRequestController>(rfh),
|
||||
enclave_manager_(
|
||||
EnclaveManagerFactory::GetAsEnclaveManagerForProfile(profile())) {
|
||||
if (enclave_manager_->is_loaded()) {
|
||||
OnEnclaveLoaded();
|
||||
return;
|
||||
}
|
||||
enclave_manager_->Load(
|
||||
base::BindOnce(&PasskeyUpgradeRequestController::OnEnclaveLoaded,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
// Copyright 2024 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef CHROME_BROWSER_UI_WEBAUTHN_PASSKEY_UPGRADE_REQUEST_CONTROLLER_H_
|
||||
#define CHROME_BROWSER_UI_WEBAUTHN_PASSKEY_UPGRADE_REQUEST_CONTROLLER_H_
|
||||
|
||||
#include "chrome/browser/webauthn/gpm_enclave_controller.h"
|
||||
#include "components/password_manager/core/browser/password_store/password_store_consumer.h"
|
||||
#include "content/public/browser/document_user_data.h"
|
||||
|
||||
namespace content {
|
||||
class RenderFrameHost;
|
||||
}
|
||||
|
||||
namespace device::enclave {
|
||||
struct CredentialRequest;
|
||||
enum class PINValidationResult;
|
||||
} // namespace device::enclave
|
||||
|
||||
class EnclaveManager;
|
||||
class GPMEnclaveTransaction;
|
||||
class Profile;
|
||||
|
||||
// PasskeyUpgradeRequestController is responsible for handling a request to
|
||||
// silently create a passkey in GPM, effectively upgrading an existing password.
|
||||
// This is also known also "conditionalCreate" in WebAuthn spec terms.
|
||||
class PasskeyUpgradeRequestController
|
||||
: public content::DocumentUserData<PasskeyUpgradeRequestController>,
|
||||
public password_manager::PasswordStoreConsumer,
|
||||
public GPMEnclaveTransaction::Delegate {
|
||||
public:
|
||||
using Callback = base::OnceCallback<void(bool success)>;
|
||||
using EnclaveRequestCallback = base::RepeatingCallback<void(
|
||||
std::unique_ptr<device::enclave::CredentialRequest>)>;
|
||||
|
||||
~PasskeyUpgradeRequestController() override;
|
||||
|
||||
void InitializeEnclaveRequestCallback(
|
||||
device::FidoDiscoveryFactory* discovery_factory);
|
||||
|
||||
// Attempts to create a passkey for the given WebAuthn RP ID and user name, if
|
||||
// a matching password exists.
|
||||
void TryUpgradePasswordToPasskey(std::string rp_id,
|
||||
const std::string& user_name,
|
||||
Callback callback);
|
||||
|
||||
private:
|
||||
enum class EnclaveState {
|
||||
kUnknown,
|
||||
kNotReady,
|
||||
kReady,
|
||||
};
|
||||
|
||||
explicit PasskeyUpgradeRequestController(content::RenderFrameHost* rfh);
|
||||
|
||||
friend DocumentUserData;
|
||||
DOCUMENT_USER_DATA_KEY_DECL();
|
||||
|
||||
// password_manager::PasswordStoreConsumer:
|
||||
void OnGetPasswordStoreResultsOrErrorFrom(
|
||||
password_manager::PasswordStoreInterface* store,
|
||||
password_manager::LoginsResultOrError results_or_error) override;
|
||||
|
||||
// GPMEnclaveTransaction::Delegate:
|
||||
void HandleEnclaveTransactionError() override;
|
||||
void BuildUVKeyOptions(EnclaveManager::UVKeyOptions& options) override;
|
||||
void HandlePINValidationResult(
|
||||
device::enclave::PINValidationResult result) override;
|
||||
void OnPasskeyCreated(
|
||||
const sync_pb::WebauthnCredentialSpecifics& passkey) override;
|
||||
|
||||
Profile* profile() const;
|
||||
|
||||
void OnEnclaveLoaded();
|
||||
void ContinuePendingUpgradeRequest();
|
||||
|
||||
raw_ptr<EnclaveManager> enclave_manager_;
|
||||
EnclaveState enclave_state_ = EnclaveState::kUnknown;
|
||||
bool pending_upgrade_request_ = false;
|
||||
|
||||
std::string rp_id_;
|
||||
std::u16string user_name_;
|
||||
Callback pending_callback_;
|
||||
|
||||
EnclaveRequestCallback enclave_request_callback_;
|
||||
|
||||
std::unique_ptr<GPMEnclaveTransaction> enclave_transaction_;
|
||||
|
||||
base::WeakPtrFactory<PasskeyUpgradeRequestController> weak_factory_{this};
|
||||
};
|
||||
|
||||
#endif // CHROME_BROWSER_UI_WEBAUTHN_PASSKEY_UPGRADE_REQUEST_CONTROLLER_H_
|
@ -42,6 +42,7 @@
|
||||
#include "chrome/browser/profiles/profile.h"
|
||||
#include "chrome/browser/signin/signin_ui_util.h"
|
||||
#include "chrome/browser/ui/webauthn/ambient/ambient_signin_controller.h"
|
||||
#include "chrome/browser/ui/webauthn/passkey_upgrade_request_controller.h"
|
||||
#include "chrome/browser/ui/webauthn/user_actions.h"
|
||||
#include "chrome/browser/webauthn/authenticator_reference.h"
|
||||
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
|
||||
@ -712,11 +713,18 @@ void AuthenticatorRequestDialogController::StartFlow(
|
||||
PopulateMechanisms();
|
||||
model_->priority_mechanism_index = IndexOfPriorityMechanism();
|
||||
|
||||
if (ui_presentation_ == UIPresentation::kAutofill) {
|
||||
// This is a conditional mediation request.
|
||||
StartAutofillRequest();
|
||||
} else {
|
||||
StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection();
|
||||
switch (ui_presentation_) {
|
||||
case UIPresentation::kModal:
|
||||
StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection();
|
||||
break;
|
||||
case UIPresentation::kAutofill:
|
||||
StartAutofillRequest();
|
||||
break;
|
||||
case UIPresentation::kPasskeyUpgrade:
|
||||
StartPasskeyUpgradeRequest();
|
||||
break;
|
||||
case UIPresentation::kDisabled:
|
||||
NOTREACHED();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2547,3 +2555,28 @@ content::RenderFrameHost*
|
||||
AuthenticatorRequestDialogController::GetRenderFrameHost() const {
|
||||
return content::RenderFrameHost::FromID(frame_host_id_);
|
||||
}
|
||||
|
||||
void AuthenticatorRequestDialogController::StartPasskeyUpgradeRequest() {
|
||||
auto* controller =
|
||||
PasskeyUpgradeRequestController::GetOrCreateForCurrentDocument(
|
||||
GetRenderFrameHost());
|
||||
if (!model_->user_entity.name) {
|
||||
FIDO_LOG(ERROR) << "Ignoring passkey upgrade request: empty username";
|
||||
return;
|
||||
}
|
||||
controller->TryUpgradePasswordToPasskey(
|
||||
model_->relying_party_id, *model_->user_entity.name,
|
||||
base::BindOnce(
|
||||
[](base::WeakPtr<AuthenticatorRequestDialogController> controller,
|
||||
bool success) {
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
// The pending request callback is resolved through the
|
||||
// MakeCredentialRequestHandler.
|
||||
FIDO_LOG(EVENT)
|
||||
<< "Passkey upgrade request complete success=" << success;
|
||||
},
|
||||
weak_factory_.GetWeakPtr()));
|
||||
SetCurrentStep(Step::kPasskeyUpgrade);
|
||||
}
|
||||
|
@ -423,6 +423,7 @@ class AuthenticatorRequestDialogController
|
||||
void ContactPhoneAfterBleIsPowered(std::string name);
|
||||
|
||||
void StartAutofillRequest();
|
||||
void StartPasskeyUpgradeRequest();
|
||||
|
||||
void DispatchRequestAsync(AuthenticatorReference* authenticator);
|
||||
|
||||
|
@ -215,6 +215,7 @@ std::ostream& operator<<(std::ostream& os,
|
||||
constexpr auto kStepNames = base::MakeFixedFlatMap<Step, std::string_view>({
|
||||
{Step::kNotStarted, "kNotStarted"},
|
||||
{Step::kPasskeyAutofill, "kPasskeyAutofill"},
|
||||
{Step::kPasskeyUpgrade, "kPasskeyUpgrade"},
|
||||
{Step::kMechanismSelection, "kMechanismSelection"},
|
||||
{Step::kErrorNoAvailableTransports, "kErrorNoAvailableTransports"},
|
||||
{Step::kErrorNoPasskeys, "kErrorNoPasskeys"},
|
||||
|
@ -200,6 +200,10 @@ struct AuthenticatorRequestDialogModel
|
||||
// dialog is shown, instead credentials are offered to the user on the
|
||||
// password autofill prompt.
|
||||
kPasskeyAutofill,
|
||||
// During passkey upgrade (i.e. WebAuthn create() with conditional
|
||||
// mediation), the WebAuthn tab-modal dialog is not used. A separate dialog
|
||||
// controller implements its own UI.
|
||||
kPasskeyUpgrade,
|
||||
kMechanismSelection,
|
||||
// The request errored out before completing. Error will only be sent
|
||||
// after user interaction.
|
||||
|
@ -50,6 +50,7 @@
|
||||
#include "chrome/browser/ui/browser_navigator_params.h"
|
||||
#include "chrome/browser/ui/browser_window.h"
|
||||
#include "chrome/browser/ui/passwords/passwords_client_ui_delegate.h"
|
||||
#include "chrome/browser/ui/webauthn/passkey_upgrade_request_controller.h"
|
||||
#include "chrome/browser/ui/webauthn/user_actions.h"
|
||||
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
|
||||
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
|
||||
@ -1090,7 +1091,9 @@ void ChromeAuthenticatorRequestDelegate::ConfigureDiscoveries(
|
||||
}
|
||||
|
||||
if (browser_provided_passkeys_available && !IsVirtualEnvironmentEnabled() &&
|
||||
request_source == RequestSource::kWebAuthentication) {
|
||||
request_source == RequestSource::kWebAuthentication &&
|
||||
dialog_controller_->ui_presentation() !=
|
||||
UIPresentation::kPasskeyUpgrade) {
|
||||
// Creating credentials in GPM can be disabled by policy, but get() is
|
||||
// always allowed.
|
||||
if (request_type == device::FidoRequestType::kGetAssertion ||
|
||||
@ -1248,6 +1251,19 @@ void ChromeAuthenticatorRequestDelegate::ConfigureDiscoveries(
|
||||
enclave_controller_->ConfigureDiscoveries(discovery_factory);
|
||||
}
|
||||
|
||||
if (dialog_controller_->ui_presentation() ==
|
||||
UIPresentation::kPasskeyUpgrade) {
|
||||
// PasskeyUpgradeController drives enclave interaction during upgrade
|
||||
// requests (conditional create). GPMEnclaveController must not be
|
||||
// instantiated.
|
||||
// TODO(crbug.com/377758786): Ensure all non-GPM discoveries are disabled
|
||||
// for passkey upgrade requests.
|
||||
CHECK(!enclave_controller_);
|
||||
PasskeyUpgradeRequestController::GetOrCreateForCurrentDocument(
|
||||
GetRenderFrameHost())
|
||||
->InitializeEnclaveRequestCallback(discovery_factory);
|
||||
}
|
||||
|
||||
dialog_controller_->set_is_non_webauthn_request(
|
||||
request_source != RequestSource::kWebAuthentication);
|
||||
|
||||
|
@ -42,6 +42,7 @@
|
||||
#include "base/thread_annotations.h"
|
||||
#include "base/time/time.h"
|
||||
#include "build/build_config.h"
|
||||
#include "chrome/browser/password_manager/profile_password_store_factory.h"
|
||||
#include "chrome/browser/signin/identity_manager_factory.h"
|
||||
#include "chrome/browser/signin/identity_test_environment_profile_adaptor.h"
|
||||
#include "chrome/browser/sync/sync_service_factory.h"
|
||||
@ -68,6 +69,8 @@
|
||||
#include "components/keyed_service/content/browser_context_dependency_manager.h"
|
||||
#include "components/network_session_configurator/common/network_switches.h"
|
||||
#include "components/os_crypt/sync/os_crypt_mocker.h"
|
||||
#include "components/password_manager/core/browser/password_form.h"
|
||||
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
|
||||
#include "components/password_manager/core/common/password_manager_pref_names.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "components/signin/public/base/consent_level.h"
|
||||
@ -496,6 +499,23 @@ static constexpr char kGetAssertionConditionalUI[] = R"((() => {
|
||||
e => window.domAutomationController.send('error ' + e));
|
||||
})())";
|
||||
|
||||
static constexpr char kMakeCredentialConditionalCreate[] = R"((() => {
|
||||
return navigator.credentials.create({
|
||||
mediation: "conditional",
|
||||
publicKey: {
|
||||
rp: { name: "www.example.com" },
|
||||
user: {
|
||||
id: new Uint8Array([1]),
|
||||
name: "bar@example.com",
|
||||
displayName: "Foo Bar"
|
||||
},
|
||||
pubKeyCredParams: [{type: "public-key", alg: -7}],
|
||||
challenge: new Uint8Array([0]),
|
||||
}
|
||||
}).then(c => window.domAutomationController.send('webauthn: ' + c.id),
|
||||
e => window.domAutomationController.send('error ' + e));
|
||||
})())";
|
||||
|
||||
bool IsReady(GPMEnclaveController::AccountState state) {
|
||||
switch (state) {
|
||||
case GPMEnclaveController::AccountState::kReady:
|
||||
@ -4071,6 +4091,86 @@ IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorWithPinBrowserTest,
|
||||
// the fix for https://crbug.com/352532554.
|
||||
}
|
||||
|
||||
class EnclaveAuthenticatorConditionalCreateBrowserTest
|
||||
: public EnclaveAuthenticatorWithPinBrowserTest {
|
||||
protected:
|
||||
EnclaveAuthenticatorConditionalCreateBrowserTest() {
|
||||
scoped_feature_list_.InitAndEnableFeature(device::kWebAuthnPasskeyUpgrade);
|
||||
CHECK(base::FeatureList::IsEnabled(device::kWebAuthnPasskeyUpgrade));
|
||||
CHECK(base::FeatureList::IsEnabled(device::kWebAuthnEnclaveAuthenticator));
|
||||
}
|
||||
|
||||
// Creates a credential to ensure the enclave authenticator is in a usable
|
||||
// state prior to making a conditional create request.
|
||||
void BootstrapEnclave() {
|
||||
trusted_vault::DownloadAuthenticationFactorsRegistrationStateResult
|
||||
registration_state_result;
|
||||
registration_state_result.state = trusted_vault::
|
||||
DownloadAuthenticationFactorsRegistrationStateResult::State::kEmpty;
|
||||
SetMockVaultConnectionOnRequestDelegate(
|
||||
std::move(registration_state_result));
|
||||
|
||||
content::WebContents* web_contents =
|
||||
browser()->tab_strip_model()->GetActiveWebContents();
|
||||
content::DOMMessageQueue message_queue(web_contents);
|
||||
content::ExecuteScriptAsync(web_contents, kMakeCredentialUvRequired);
|
||||
delegate_observer()->WaitForUI();
|
||||
|
||||
EXPECT_EQ(dialog_model()->step(),
|
||||
AuthenticatorRequestDialogModel::Step::kGPMCreatePasskey);
|
||||
EXPECT_EQ(request_delegate()
|
||||
->enclave_controller_for_testing()
|
||||
->account_state_for_testing(),
|
||||
GPMEnclaveController::AccountState::kEmpty);
|
||||
dialog_model()->OnGPMCreatePasskey();
|
||||
EXPECT_EQ(dialog_model()->step(),
|
||||
AuthenticatorRequestDialogModel::Step::kGPMCreatePin);
|
||||
dialog_model()->OnGPMPinEntered(u"123456");
|
||||
|
||||
std::string script_result;
|
||||
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
|
||||
EXPECT_EQ(script_result, "\"webauthn: uv=true\"");
|
||||
}
|
||||
|
||||
void InjectPassword(base::Time last_used) {
|
||||
password_manager::PasswordStoreInterface* password_store =
|
||||
ProfilePasswordStoreFactory::GetForProfile(
|
||||
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
|
||||
.get();
|
||||
password_manager::PasswordForm saved_form;
|
||||
saved_form.signon_realm = https_server_.GetURL("example.com", "/").spec();
|
||||
saved_form.url = https_server_.GetURL("example.com",
|
||||
"/password/prefilled_username.html");
|
||||
saved_form.username_value = u"bar@example.com";
|
||||
saved_form.password_value = u"hunter1";
|
||||
saved_form.date_last_used = last_used;
|
||||
password_store->AddLogin(saved_form);
|
||||
}
|
||||
|
||||
base::test::ScopedFeatureList scoped_feature_list_;
|
||||
};
|
||||
|
||||
IN_PROC_BROWSER_TEST_F(EnclaveAuthenticatorConditionalCreateBrowserTest,
|
||||
ConditionalCreate) {
|
||||
ASSERT_TRUE(ui_test_utils::NavigateToURL(
|
||||
browser(), https_server_.GetURL("www.example.com", "/title1.html")));
|
||||
|
||||
BootstrapEnclave();
|
||||
InjectPassword(base::Time::Now());
|
||||
|
||||
content::WebContents* web_contents =
|
||||
browser()->tab_strip_model()->GetActiveWebContents();
|
||||
content::DOMMessageQueue message_queue(web_contents);
|
||||
content::ExecuteScriptAsync(web_contents, kMakeCredentialConditionalCreate);
|
||||
delegate_observer()->WaitForUI();
|
||||
|
||||
std::string script_result;
|
||||
ASSERT_TRUE(message_queue.WaitForMessage(&script_result));
|
||||
std::optional<std::vector<uint8_t>> cred_id =
|
||||
ParseCredentialId(script_result);
|
||||
ASSERT_TRUE(cred_id);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // !defined(MEMORY_SANITIZER)
|
||||
|
@ -144,7 +144,7 @@ TEST(WebAuthenticationJSONConversionTest,
|
||||
/*provider_scope_requested=*/true,
|
||||
device::AttestationConveyancePreference::kDirect,
|
||||
std::vector<std::string>({"a", "b", "c"})),
|
||||
std::vector<std::string>{"attfmt1", "attfmt2"});
|
||||
std::vector<std::string>{"attfmt1", "attfmt2"}, /*is_conditional=*/false);
|
||||
|
||||
base::Value value = ToValue(options);
|
||||
std::string json;
|
||||
|
@ -982,6 +982,13 @@ void AuthenticatorCommonImpl::MakeCredential(
|
||||
|
||||
if (options->is_payment_credential_creation) {
|
||||
req_state_->mode = AuthenticationRequestMode::kPayment;
|
||||
} else if (options->is_conditional) {
|
||||
if (!base::FeatureList::IsEnabled(device::kWebAuthnPasskeyUpgrade)) {
|
||||
// The renderer runtime flag should enforce this.
|
||||
mojo::ReportBadMessage("kWebAuthnPasskeyUpgrade flag must be enabled");
|
||||
return;
|
||||
}
|
||||
req_state_->mode = AuthenticationRequestMode::kPasskeyUpgrade;
|
||||
} else {
|
||||
req_state_->mode = AuthenticationRequestMode::kModalWebAuthn;
|
||||
}
|
||||
@ -1206,8 +1213,12 @@ void AuthenticatorCommonImpl::ContinueMakeCredentialAfterRpIdCheck(
|
||||
{*cred_protect_request, options->enforce_protection_policy}};
|
||||
}
|
||||
|
||||
auto ui_presentation =
|
||||
disable_ui_ ? UIPresentation::kDisabled : UIPresentation::kModal;
|
||||
auto ui_presentation = UIPresentation::kModal;
|
||||
if (disable_ui_) {
|
||||
ui_presentation = UIPresentation::kDisabled;
|
||||
} else if (options->is_conditional) {
|
||||
ui_presentation = UIPresentation::kPasskeyUpgrade;
|
||||
}
|
||||
req_state_->request_delegate->SetUIPresentation(ui_presentation);
|
||||
|
||||
// Assemble clientDataJSON.
|
||||
|
@ -67,6 +67,7 @@ enum class AuthenticationRequestMode {
|
||||
kModalWebAuthn = 0,
|
||||
kConditional = 1,
|
||||
kPayment = 2,
|
||||
kPasskeyUpgrade = 3,
|
||||
};
|
||||
|
||||
} // namespace content
|
||||
|
@ -286,6 +286,8 @@ void SetRuntimeFeaturesFromChromiumFeatures() {
|
||||
raw_ref(features::kUserActivationSameOriginVisibility)},
|
||||
{wf::EnableWebAuthenticationAmbient,
|
||||
raw_ref(device::kWebAuthnAmbientSignin)},
|
||||
{wf::EnableWebAuthenticationConditionalCreate,
|
||||
raw_ref(device::kWebAuthnPasskeyUpgrade)},
|
||||
{wf::EnableWebBluetooth, raw_ref(features::kWebBluetooth),
|
||||
kSetOnlyIfOverridden},
|
||||
{wf::EnableWebBluetoothGetDevices,
|
||||
|
@ -249,6 +249,9 @@ class CONTENT_EXPORT AuthenticatorRequestClientDelegate
|
||||
kModal,
|
||||
// Passkey autofill UI for .get() requests with `mediation = "conditional"`.
|
||||
kAutofill,
|
||||
// Passkey upgrade request, i.e. .create() requests with `mediation =
|
||||
// "conditional"`.
|
||||
kPasskeyUpgrade,
|
||||
// No WebAuthn UI shown. This is used for some internal requests that
|
||||
// originate outside of WebAuthn (e.g. payments) and provide their own
|
||||
// request UI.
|
||||
|
@ -163,4 +163,8 @@ BASE_FEATURE(kDigitalCredentialsHybridLinking,
|
||||
"DigitalCredentialsHybridLinking",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
|
||||
BASE_FEATURE(kWebAuthnPasskeyUpgrade,
|
||||
"WebAuthenticationPasskeyUpgrade",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
|
||||
} // namespace device
|
||||
|
@ -134,6 +134,10 @@ BASE_DECLARE_FEATURE(kWebAuthnSkipHybridConfigIfSystemSupported);
|
||||
COMPONENT_EXPORT(DEVICE_FIDO)
|
||||
BASE_DECLARE_FEATURE(kDigitalCredentialsHybridLinking);
|
||||
|
||||
// Enable passkey upgrade requests in Google Password Manager.
|
||||
COMPONENT_EXPORT(DEVICE_FIDO)
|
||||
BASE_DECLARE_FEATURE(kWebAuthnPasskeyUpgrade);
|
||||
|
||||
} // namespace device
|
||||
|
||||
#endif // DEVICE_FIDO_FEATURES_H_
|
||||
|
@ -564,6 +564,8 @@ struct PublicKeyCredentialCreationOptions {
|
||||
|
||||
// https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptionsjson-attestationformats
|
||||
array<string> attestation_formats;
|
||||
|
||||
bool is_conditional = false;
|
||||
};
|
||||
|
||||
// See https://w3c.github.io/webauthn/#enumdef-attestationconveyancepreference
|
||||
|
58
third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc
vendored
58
third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc
vendored
@ -1902,38 +1902,44 @@ AuthenticationCredentialsContainer::create(
|
||||
resolver->Reject(MakeGarbageCollected<DOMException>(
|
||||
DOMExceptionCode::kNotSupportedError,
|
||||
"Required parameters missing in `options.publicKey`."));
|
||||
} else if (mojo_options->user->id.size() > 64) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (mojo_options->user->id.size() > 64) {
|
||||
// https://www.w3.org/TR/webauthn/#user-handle
|
||||
v8::Isolate* isolate = resolver->GetScriptState()->GetIsolate();
|
||||
resolver->Reject(V8ThrowException::CreateTypeError(
|
||||
isolate, "User handle exceeds 64 bytes."));
|
||||
} else {
|
||||
if (!mojo_options->relying_party->id) {
|
||||
mojo_options->relying_party->id =
|
||||
resolver->GetExecutionContext()->GetSecurityOrigin()->Domain();
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
auto* authenticator =
|
||||
CredentialManagerProxy::From(script_state)->Authenticator();
|
||||
if (mojo_options->is_payment_credential_creation) {
|
||||
String rp_id_for_payment_extension = mojo_options->relying_party->id;
|
||||
WTF::Vector<uint8_t> user_id_for_payment_extension =
|
||||
mojo_options->user->id;
|
||||
authenticator->MakeCredential(
|
||||
std::move(mojo_options),
|
||||
WTF::BindOnce(&OnMakePublicKeyCredentialWithPaymentExtensionComplete,
|
||||
std::make_unique<ScopedPromiseResolver>(resolver),
|
||||
std::move(scoped_abort_state),
|
||||
rp_id_for_payment_extension,
|
||||
std::move(user_id_for_payment_extension)));
|
||||
} else {
|
||||
authenticator->MakeCredential(
|
||||
std::move(mojo_options),
|
||||
WTF::BindOnce(&OnMakePublicKeyCredentialComplete,
|
||||
std::make_unique<ScopedPromiseResolver>(resolver),
|
||||
std::move(scoped_abort_state), required_origin_type,
|
||||
is_rk_required));
|
||||
if (!mojo_options->relying_party->id) {
|
||||
mojo_options->relying_party->id =
|
||||
resolver->GetExecutionContext()->GetSecurityOrigin()->Domain();
|
||||
}
|
||||
|
||||
auto* authenticator =
|
||||
CredentialManagerProxy::From(script_state)->Authenticator();
|
||||
if (mojo_options->is_payment_credential_creation) {
|
||||
String rp_id_for_payment_extension = mojo_options->relying_party->id;
|
||||
WTF::Vector<uint8_t> user_id_for_payment_extension = mojo_options->user->id;
|
||||
authenticator->MakeCredential(
|
||||
std::move(mojo_options),
|
||||
WTF::BindOnce(&OnMakePublicKeyCredentialWithPaymentExtensionComplete,
|
||||
std::make_unique<ScopedPromiseResolver>(resolver),
|
||||
std::move(scoped_abort_state),
|
||||
rp_id_for_payment_extension,
|
||||
std::move(user_id_for_payment_extension)));
|
||||
} else {
|
||||
if (RuntimeEnabledFeatures::WebAuthenticationConditionalCreateEnabled()) {
|
||||
mojo_options->is_conditional = options->mediation() == "conditional";
|
||||
}
|
||||
authenticator->MakeCredential(
|
||||
std::move(mojo_options),
|
||||
WTF::BindOnce(&OnMakePublicKeyCredentialComplete,
|
||||
std::make_unique<ScopedPromiseResolver>(resolver),
|
||||
std::move(scoped_abort_state), required_origin_type,
|
||||
is_rk_required));
|
||||
}
|
||||
|
||||
return promise;
|
||||
|
@ -12,5 +12,7 @@ dictionary CredentialCreationOptions {
|
||||
PasswordCredentialInit password;
|
||||
FederatedCredentialInit federated;
|
||||
PublicKeyCredentialCreationOptions publicKey;
|
||||
[RuntimeEnabled=WebAuthenticationConditionalCreate]
|
||||
CredentialMediationRequirement mediation = "optional";
|
||||
AbortSignal signal;
|
||||
};
|
||||
|
@ -4752,6 +4752,13 @@
|
||||
name: "WebAuthenticationClientCapabilities",
|
||||
status: "experimental",
|
||||
},
|
||||
// https://w3c.github.io/webauthn/#dom-clientcapability-conditionalcreate
|
||||
{
|
||||
name: "WebAuthenticationConditionalCreate",
|
||||
status: "test",
|
||||
base_feature: "none",
|
||||
public: true,
|
||||
},
|
||||
// Methods for deserializing WebAuthn requests from JSON/serializing
|
||||
// responses into JSON.
|
||||
// https://w3c.github.io/webauthn/#dom-publickeycredential-tojson
|
||||
|
@ -298,6 +298,7 @@ chromium-metrics-reviews@google.com.
|
||||
<int value="0" label="WebAuthn Modal Request"/>
|
||||
<int value="1" label="WebAuthn Conditional UI Request"/>
|
||||
<int value="2" label="Payment Request"/>
|
||||
<int value="3" label="WebAuthn Passkey Upgrade Request"/>
|
||||
</enum>
|
||||
|
||||
<enum name="WindowsForegroundedHelloDialog">
|
||||
|
Reference in New Issue
Block a user