blob: 79b56382b8a4ba7e272321fe5637f0e2caf12672 [file] [log] [blame]
// Copyright 2021 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/password_manager/chrome_webauthn_credentials_delegate.h"
#include <optional>
#include "base/base64.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "content/public/browser/web_contents.h"
#include "device/fido/fido_types.h"
#include "ui/base/l10n/l10n_util.h"
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/webauthn/authenticator_request_dialog_controller.h"
#include "chrome/browser/webauthn/authenticator_request_scheduler.h"
#include "chrome/browser/webauthn/chrome_authenticator_request_delegate.h"
#endif // !BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/webauthn/android/webauthn_request_delegate_android.h"
#endif
namespace {
using device::AuthenticatorType;
#if !BUILDFLAG(IS_ANDROID)
// `AuthenticatorRequestDialogModel` observed from `authenticator_observation_`
// may notify this class too soon, causing a flicker. Delay calling
// `passkey_selected_callback_` at least 300ms to avoid the flicker.
// TODO(crbug.com/332619045): Move this to a UI layer.
constexpr base::TimeDelta kFlickerDuration = base::Milliseconds(300);
bool IsGpmPasskeyAuthenticatorType(AuthenticatorType type) {
return type == AuthenticatorType::kEnclave;
}
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
using password_manager::PasskeyCredential;
using OnPasskeySelectedCallback =
password_manager::WebAuthnCredentialsDelegate::OnPasskeySelectedCallback;
ChromeWebAuthnCredentialsDelegate::ChromeWebAuthnCredentialsDelegate(
content::WebContents* web_contents)
: web_contents_(web_contents) {}
ChromeWebAuthnCredentialsDelegate::~ChromeWebAuthnCredentialsDelegate() =
default;
void ChromeWebAuthnCredentialsDelegate::LaunchSecurityKeyOrHybridFlow() {
#if !BUILDFLAG(IS_ANDROID)
ChromeAuthenticatorRequestDelegate* authenticator_delegate =
AuthenticatorRequestScheduler::GetRequestDelegate(web_contents_);
if (!authenticator_delegate) {
return;
}
authenticator_delegate->dialog_controller()
->TransitionToModalWebAuthnRequest();
#else
if (WebAuthnRequestDelegateAndroid* delegate =
WebAuthnRequestDelegateAndroid::GetRequestDelegate(web_contents_)) {
delegate->OnHybridSignInSelected();
}
#endif // !BUILDFLAG(IS_ANDROID)
}
void ChromeWebAuthnCredentialsDelegate::SelectPasskey(
const std::string& backend_id,
OnPasskeySelectedCallback callback) {
// `backend_id` is the base64-encoded credential ID. See `PasskeyCredential`
// for where these are encoded.
std::optional<std::vector<uint8_t>> selected_credential_id =
base::Base64Decode(backend_id);
DCHECK(selected_credential_id);
#if BUILDFLAG(IS_ANDROID)
std::move(callback).Run();
auto* request_delegate =
WebAuthnRequestDelegateAndroid::GetRequestDelegate(web_contents_);
if (!request_delegate) {
return;
}
request_delegate->OnWebAuthnAccountSelected(*selected_credential_id);
#else
ChromeAuthenticatorRequestDelegate* authenticator_delegate =
AuthenticatorRequestScheduler::GetRequestDelegate(web_contents_);
if (!authenticator_delegate) {
std::move(callback).Run();
return;
}
if (passkey_selected_callback_) {
// The user tapped on another passkey while the enclave was loading. Ignore
// the tap.
// TODO(crbug.com/344950143): Disable the rows that are not supposed to be
// clicked.
return;
}
passkey_selected_callback_ = std::move(callback);
authenticator_observation_.Observe(authenticator_delegate->dialog_model());
AuthenticatorType credential_source =
authenticator_delegate->dialog_controller()->OnAccountPreselected(
*selected_credential_id);
// If the credential is not from a GPM authenticator, we do not need to
// observe the model anymore.
if (!IsGpmPasskeyAuthenticatorType(credential_source)) {
authenticator_observation_.Reset();
std::move(passkey_selected_callback_).Run();
}
#endif // BUILDFLAG(IS_ANDROID)
}
base::expected<const std::vector<PasskeyCredential>*,
ChromeWebAuthnCredentialsDelegate::PasskeysUnavailableReason>
ChromeWebAuthnCredentialsDelegate::GetPasskeys() const {
if (!passkeys_) {
return last_request_was_aborted_
? base::unexpected(
ChromeWebAuthnCredentialsDelegate::
PasskeysUnavailableReason::kRequestAborted)
: base::unexpected(ChromeWebAuthnCredentialsDelegate::
PasskeysUnavailableReason::kNotReceived);
}
return base::ok(&passkeys_.value());
}
void ChromeWebAuthnCredentialsDelegate::NotifyForPasskeysDisplay() {
passkey_display_has_happened_ = true;
}
base::WeakPtr<password_manager::WebAuthnCredentialsDelegate>
ChromeWebAuthnCredentialsDelegate::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool ChromeWebAuthnCredentialsDelegate::HasPendingPasskeySelection() {
#if BUILDFLAG(IS_ANDROID)
return false;
#else
return !passkey_selected_callback_.is_null();
#endif // BUILDFLAG(IS_ANDROID)
}
#if !BUILDFLAG(IS_ANDROID)
void ChromeWebAuthnCredentialsDelegate::OnStepTransition() {
AuthenticatorRequestDialogModel* model =
authenticator_observation_.GetSource();
CHECK(model) << "The model just stepped but not registered as source.";
if (!passkey_selected_callback_ || !model->preselected_cred.has_value() ||
!IsGpmPasskeyAuthenticatorType(model->preselected_cred.value().source)) {
return;
}
// Do not dismiss the autofill popup when the AuthenticatorRequestDialogModel
// says that UI is disabled.
if (!model->ui_disabled_ &&
model->step() != AuthenticatorRequestDialogModel::Step::kNotStarted) {
authenticator_observation_.Reset();
flickering_timer_.Start(FROM_HERE, kFlickerDuration,
std::move(passkey_selected_callback_));
}
}
#endif // !BUILDFLAG(IS_ANDROID)
bool ChromeWebAuthnCredentialsDelegate::IsSecurityKeyOrHybridFlowAvailable()
const {
return security_key_or_hybrid_flow_available_.value();
}
void ChromeWebAuthnCredentialsDelegate::RequestNotificationWhenPasskeysReady(
base::OnceClosure callback) {
if (!passkey_retrieval_timer_started_) {
passkey_retrieval_timer_ = std::make_unique<base::ElapsedTimer>();
passkey_retrieval_timer_started_ = true;
}
if (passkeys_.has_value()) {
RecordPasskeyRetrievalDelay();
// Entries were already populated from the WebAuthn request.
std::move(callback).Run();
return;
}
passkeys_available_callbacks_.push_back(std::move(callback));
}
void ChromeWebAuthnCredentialsDelegate::OnCredentialsReceived(
std::vector<PasskeyCredential> credentials,
SecurityKeyOrHybridFlowAvailable security_key_or_hybrid_flow_available) {
last_request_was_aborted_ = false;
if (!credentials.empty() && !passkeys_after_fill_recorded_) {
passkeys_after_fill_recorded_ = true;
base::UmaHistogramBoolean(
"PasswordManager.PasskeysArrivedAfterAutofillDisplay",
passkey_display_has_happened_);
}
passkeys_ = std::move(credentials);
security_key_or_hybrid_flow_available_ =
security_key_or_hybrid_flow_available;
RecordPasskeyRetrievalDelay();
NotifyClientsOfPasskeyAvailability();
}
void ChromeWebAuthnCredentialsDelegate::NotifyWebAuthnRequestAborted() {
passkeys_ = std::nullopt;
last_request_was_aborted_ = true;
NotifyClientsOfPasskeyAvailability();
#if !BUILDFLAG(IS_ANDROID)
// Also dismiss the autofill popup if it is being displayed and a webauthn
// request is loading.
if (passkey_selected_callback_) {
flickering_timer_.Start(FROM_HERE, kFlickerDuration,
std::move(passkey_selected_callback_));
}
authenticator_observation_.Reset();
#endif // BUILDFLAG(IS_ANDROID)
}
void ChromeWebAuthnCredentialsDelegate::RecordPasskeyRetrievalDelay() {
if (passkey_retrieval_timer_) {
base::UmaHistogramTimes("PasswordManager.PasskeyRetrievalWaitDuration",
passkey_retrieval_timer_->Elapsed());
passkey_retrieval_timer_.reset();
}
}
void ChromeWebAuthnCredentialsDelegate::NotifyClientsOfPasskeyAvailability() {
std::vector<base::OnceClosure> callbacks;
callbacks.swap(passkeys_available_callbacks_);
for (auto& callback : callbacks) {
std::move(callback).Run();
}
}