0

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:
Martin Kreichgauer
2024-11-19 22:22:26 +00:00
committed by Chromium LUCI CQ
parent 39497a60f0
commit 980dcfb104
22 changed files with 501 additions and 35 deletions

@ -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:

@ -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

@ -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">