| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/autofill/core/browser/payments/credit_card_access_manager.h" |
| |
| #include <algorithm> |
| #include <functional> |
| #include <memory> |
| #include <set> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_deref.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "components/autofill/core/browser/autofill_progress_dialog_type.h" |
| #include "components/autofill/core/browser/data_manager/payments/payments_data_manager.h" |
| #include "components/autofill/core/browser/data_model/payments/credit_card.h" |
| #include "components/autofill/core/browser/form_import/form_data_importer.h" |
| #include "components/autofill/core/browser/foundations/autofill_client.h" |
| #include "components/autofill/core/browser/foundations/autofill_driver.h" |
| #include "components/autofill/core/browser/foundations/browser_autofill_manager.h" |
| #include "components/autofill/core/browser/metrics/form_events/credit_card_form_event_logger.h" |
| #include "components/autofill/core/browser/metrics/payments/better_auth_metrics.h" |
| #include "components/autofill/core/browser/metrics/payments/card_info_retrieval_enrolled_metrics.h" |
| #include "components/autofill/core/browser/metrics/payments/card_unmask_authentication_metrics.h" |
| #include "components/autofill/core/browser/metrics/payments/card_unmask_flow_metrics.h" |
| #include "components/autofill/core/browser/metrics/payments/mandatory_reauth_metrics.h" |
| #include "components/autofill/core/browser/payments/autofill_error_dialog_context.h" |
| #include "components/autofill/core/browser/payments/autofill_payments_feature_availability.h" |
| #include "components/autofill/core/browser/payments/credit_card_cvc_authenticator.h" |
| #include "components/autofill/core/browser/payments/credit_card_risk_based_authenticator.h" |
| #include "components/autofill/core/browser/payments/mandatory_reauth_manager.h" |
| #include "components/autofill/core/browser/payments/payments_autofill_client.h" |
| #include "components/autofill/core/browser/payments/payments_network_interface.h" |
| #include "components/autofill/core/browser/payments/payments_util.h" |
| #include "components/autofill/core/browser/payments/payments_window_manager.h" |
| #include "components/autofill/core/browser/payments/virtual_card_enrollment_manager.h" |
| #include "components/autofill/core/browser/payments/webauthn_callback_types.h" |
| #include "components/autofill/core/browser/suggestions/payments/payments_suggestion_generator.h" |
| #include "components/autofill/core/common/autofill_clock.h" |
| #include "components/autofill/core/common/autofill_payments_features.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| #if !BUILDFLAG(IS_IOS) |
| #include "components/autofill/core/browser/strike_databases/payments/fido_authentication_strike_database.h" |
| #endif |
| |
| namespace autofill { |
| namespace { |
| |
| using CardInfoRetrievalEnrolledUnmaskResult = |
| autofill_metrics::CardInfoRetrievalEnrolledUnmaskResult; |
| using PaymentsRpcCardType = |
| payments::PaymentsAutofillClient::PaymentsRpcCardType; |
| using PaymentsRpcResult = payments::PaymentsAutofillClient::PaymentsRpcResult; |
| using Result = |
| CreditCardRiskBasedAuthenticator::RiskBasedAuthenticationResponse::Result; |
| using ServerCardUnmaskFlowType = autofill_metrics::ServerCardUnmaskFlowType; |
| using ServerCardUnmaskResult = autofill_metrics::ServerCardUnmaskResult; |
| |
| // Timeout to wait for unmask details from Google Payments. |
| constexpr auto kUnmaskDetailsResponseTimeout = base::Seconds(3); |
| // Time to wait between multiple calls to GetUnmaskDetails(). |
| constexpr auto kDelayForGetUnmaskDetails = base::Minutes(3); |
| |
| // Suffix for server IDs in the cache indicating that a card is a virtual card. |
| constexpr char kVirtualCardIdentifier[] = "_vcn"; |
| |
| bool IsEligibleForCardInfoRetrievalAuthentication( |
| const CreditCard& card, |
| const std::vector<CardUnmaskChallengeOption>& challenge_options) { |
| if (card.card_info_retrieval_enrollment_state() != |
| CreditCard::CardInfoRetrievalEnrollmentState::kRetrievalEnrolled) { |
| return false; |
| } |
| |
| if (challenge_options.empty()) { |
| return false; |
| } |
| |
| // We currently only support SMS OTP challenge for CardInfoRetrieval. |
| for (const CardUnmaskChallengeOption& challenge_option : challenge_options) { |
| if (challenge_option.type != CardUnmaskChallengeOptionType::kSmsOtp) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| CreditCardAccessManager::CreditCardAccessManager( |
| AutofillManager* manager, |
| autofill_metrics::CreditCardFormEventLogger* form_event_logger) |
| : manager_(CHECK_DEREF(manager)), form_event_logger_(form_event_logger) {} |
| |
| CreditCardAccessManager::~CreditCardAccessManager() { |
| // This clears the record type of the most recently autofilled card with no |
| // interactive authentication flow upon page navigation, as page navigation |
| // results in us destroying the current CreditCardAccessManager and creating a |
| // new one. |
| if (auto* form_data_importer = autofill_client().GetFormDataImporter()) { |
| form_data_importer |
| ->SetPaymentMethodTypeIfNonInteractiveAuthenticationFlowCompleted( |
| std::nullopt); |
| } |
| } |
| |
| void CreditCardAccessManager::UpdateCreditCardFormEventLogger() { |
| size_t server_record_type_count = 0; |
| size_t local_record_type_count = 0; |
| for (const CreditCard* credit_card : |
| payments_data_manager().GetCreditCards()) { |
| if (credit_card->record_type() == CreditCard::RecordType::kLocalCard) { |
| local_record_type_count++; |
| } else { |
| server_record_type_count++; |
| } |
| } |
| form_event_logger_->set_server_record_type_count(server_record_type_count); |
| form_event_logger_->set_local_record_type_count(local_record_type_count); |
| } |
| |
| bool CreditCardAccessManager::UnmaskedCardCacheIsEmpty() { |
| return unmasked_card_cache_.empty(); |
| } |
| |
| std::vector<const CachedServerCardInfo*> |
| CreditCardAccessManager::GetCachedUnmaskedCards() const { |
| std::vector<const CachedServerCardInfo*> unmasked_cards; |
| for (const auto& [key, card_info] : unmasked_card_cache_) { |
| unmasked_cards.push_back(&card_info); |
| } |
| return unmasked_cards; |
| } |
| |
| bool CreditCardAccessManager::IsCardPresentInUnmaskedCache( |
| const CreditCard& card) const { |
| return unmasked_card_cache_.find(GetKeyForUnmaskedCardsCache(card)) != |
| unmasked_card_cache_.end(); |
| } |
| |
| bool CreditCardAccessManager::ShouldClearPreviewedForm() { |
| return !is_authentication_in_progress_; |
| } |
| |
| void CreditCardAccessManager::PrepareToFetchCreditCard() { |
| #if !BUILDFLAG(IS_IOS) |
| // No need to fetch details if there are no server cards. |
| if (std::ranges::all_of(GetCreditCardsToSuggest(payments_data_manager()), |
| &CreditCard::IsLocalCard)) { |
| return; |
| } |
| |
| // Do not make a preflight call if unnecessary, such as if one is already in |
| // progress or a recently-returned call should be currently used. |
| if (!can_fetch_unmask_details_) { |
| return; |
| } |
| // As we may now be making a GetUnmaskDetails call, disallow further calls |
| // until the current preflight call has been used or has timed out. |
| can_fetch_unmask_details_ = false; |
| |
| // Reset in case a late response was ignored. |
| ready_to_start_authentication_.Reset(); |
| |
| // Prefetching risk data for latency optimization and caching it for upcoming |
| // use in retrieval. |
| if (base::FeatureList::IsEnabled( |
| features::kAutofillEnablePrefetchingRiskDataForRetrieval)) { |
| autofill_client().GetPaymentsAutofillClient()->LoadRiskData( |
| base::DoNothing()); |
| } |
| |
| // If `is_user_verifiable_` is set, then directly call |
| // `GetUnmaskDetailsIfUserIsVerifiable()`, otherwise fetch value for |
| // `is_user_verifiable_`. |
| if (is_user_verifiable_.has_value()) { |
| GetUnmaskDetailsIfUserIsVerifiable(is_user_verifiable_.value()); |
| } else { |
| is_user_verifiable_called_timestamp_ = base::TimeTicks::Now(); |
| |
| GetOrCreateFidoAuthenticator()->IsUserVerifiable(base::BindOnce( |
| &CreditCardAccessManager::GetUnmaskDetailsIfUserIsVerifiable, |
| GetWeakPtr())); |
| } |
| #endif |
| } |
| |
| void CreditCardAccessManager::GetUnmaskDetailsIfUserIsVerifiable( |
| bool is_user_verifiable) { |
| #if !BUILDFLAG(IS_IOS) |
| is_user_verifiable_ = is_user_verifiable; |
| |
| if (is_user_verifiable_called_timestamp_.has_value()) { |
| autofill_metrics::LogUserVerifiabilityCheckDuration( |
| base::TimeTicks::Now() - is_user_verifiable_called_timestamp_.value()); |
| } |
| |
| // If there is already an unmask details request in progress, do not initiate |
| // another one and return early. |
| if (unmask_details_request_in_progress_) { |
| return; |
| } |
| |
| // Log that we are initiating the card unmask preflight flow. |
| autofill_metrics::LogCardUnmaskPreflightInitiated(); |
| |
| // If user is verifiable, then make preflight call to payments to fetch unmask |
| // details, otherwise the only option is to perform CVC Auth, which does not |
| // require any. |
| if (is_user_verifiable_.value_or(false)) { |
| unmask_details_request_in_progress_ = true; |
| preflight_call_timestamp_ = base::TimeTicks::Now(); |
| payments_autofill_client().GetPaymentsNetworkInterface()->GetUnmaskDetails( |
| base::BindOnce(&CreditCardAccessManager::OnDidGetUnmaskDetails, |
| GetWeakPtr()), |
| payments_data_manager().app_locale()); |
| autofill_metrics::LogCardUnmaskPreflightCalled( |
| GetOrCreateFidoAuthenticator()->IsUserOptedIn()); |
| } |
| #endif |
| } |
| |
| void CreditCardAccessManager::LogMetricsAndFillFormForServerUnmaskFlows( |
| UnmaskAuthFlowType unmask_auth_flow_type) { |
| ServerCardUnmaskFlowType flow_type = ServerCardUnmaskFlowType::kUnspecified; |
| switch (unmask_auth_flow_type) { |
| case UnmaskAuthFlowType::kCvcThenFido: |
| case UnmaskAuthFlowType::kFido: |
| form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted( |
| unmask_auth_flow_type_); |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| flow_type = ServerCardUnmaskFlowType::kFidoOnly; |
| } |
| if (!card_->cvc().empty()) { |
| autofill_metrics::LogCvcFilling( |
| autofill_metrics::CvcFillingFlowType::kFido, card_->record_type()); |
| } |
| break; |
| case UnmaskAuthFlowType::kOtp: |
| flow_type = ServerCardUnmaskFlowType::kOtpOnly; |
| break; |
| case UnmaskAuthFlowType::kOtpFallbackFromFido: |
| flow_type = ServerCardUnmaskFlowType::kOtpFallbackFromFido; |
| break; |
| case UnmaskAuthFlowType::kThreeDomainSecure: |
| // TODO(crbug.com/41494927): Add logging for kThreeDomainSecure. |
| case UnmaskAuthFlowType::kThreeDomainSecureConsentAlreadyGiven: |
| // TODO(crbug.com/41494927): Add logging for |
| // kThreeDomainSecureConsentAlreadyGiven. |
| case UnmaskAuthFlowType::kCvc: |
| case UnmaskAuthFlowType::kCvcFallbackFromFido: |
| case UnmaskAuthFlowType::kNone: |
| NOTREACHED(); |
| } |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kAuthenticationUnmasked, |
| card_->record_type() == CreditCard::RecordType::kVirtualCard |
| ? PaymentsRpcCardType::kVirtualCard |
| : PaymentsRpcCardType::kServerCard, |
| flow_type); |
| if (card_->IsEnrolledInCardInfoRetrieval() && |
| (unmask_auth_flow_type == UnmaskAuthFlowType::kOtp || |
| unmask_auth_flow_type == UnmaskAuthFlowType::kOtpFallbackFromFido)) { |
| autofill_metrics::LogCardInfoRetrievalEnrolledUnmaskResult( |
| CardInfoRetrievalEnrolledUnmaskResult::kAuthenticationUnmasked); |
| } |
| OnCreditCardFetched(*card_, /*card_was_fetched_from_cache=*/false); |
| } |
| |
| void CreditCardAccessManager::OnDidGetUnmaskDetails( |
| PaymentsRpcResult result, |
| payments::UnmaskDetails& unmask_details) { |
| // Log latency for preflight call. |
| if (preflight_call_timestamp_.has_value()) { |
| autofill_metrics::LogCardUnmaskPreflightDuration( |
| base::TimeTicks::Now() - *preflight_call_timestamp_); |
| } |
| |
| unmask_details_request_in_progress_ = false; |
| unmask_details_ = unmask_details; |
| |
| // TODO(crbug.com/40253859): Check that the user is off the record separately. |
| unmask_details_.server_denotes_fido_eligible_but_not_opted_in = |
| unmask_details_.server_denotes_fido_eligible_but_not_opted_in && |
| !autofill_client().IsOffTheRecord(); |
| |
| // Set delay as FIDO request timeout if available, otherwise set to default. |
| base::TimeDelta delay = kDelayForGetUnmaskDetails; |
| if (!unmask_details_.fido_request_options.empty()) { |
| if (std::optional<int> request_timeout = |
| unmask_details_.fido_request_options.FindInt("timeout_millis")) { |
| delay = base::Milliseconds(*request_timeout); |
| } |
| } |
| |
| #if !BUILDFLAG(IS_IOS) |
| opt_in_intention_ = |
| GetOrCreateFidoAuthenticator()->GetUserOptInIntention(unmask_details); |
| #endif |
| ready_to_start_authentication_.Signal(); |
| |
| // Use the weak_ptr here so that the delayed task won't be executed if the |
| // |credit_card_access_manager| is reset. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&CreditCardAccessManager::SignalCanFetchUnmaskDetails, |
| GetWeakPtr()), |
| delay); |
| } |
| |
| void CreditCardAccessManager::FetchCreditCard( |
| const CreditCard* card, |
| OnCreditCardFetchedCallback on_credit_card_fetched) { |
| auto* form_data_importer = autofill_client().GetFormDataImporter(); |
| CHECK(form_data_importer); |
| form_data_importer->fetched_payments_data_context() = |
| FormDataImporter::FetchedPaymentsDataContext(); |
| // Reset the variable in FormDataImporter that denotes if non-interactive |
| // authentication happened. This variable will be set to a value if a |
| // payments autofill non-interactive flow successfully completes. |
| form_data_importer |
| ->SetPaymentMethodTypeIfNonInteractiveAuthenticationFlowCompleted( |
| std::nullopt); |
| |
| // Abort if authentication is already in progress, but don't reset status. |
| if (is_authentication_in_progress_) { |
| return; |
| } |
| |
| // If card is nullptr we reset all states and abort authentication. |
| if (!card) { |
| Reset(); |
| return; |
| } |
| |
| // Get the card's record type to correctly handle its fetching. |
| CreditCard::RecordType record_type = card->record_type(); |
| on_credit_card_fetched_callback_ = std::move(on_credit_card_fetched); |
| |
| // Log the server card unmasking attempt, and differentiate based on server |
| // card or virtual card. |
| if (ShouldLogServerCardUnmaskAttemptMetrics(record_type)) { |
| autofill_metrics::LogServerCardUnmaskAttempt( |
| record_type == CreditCard::RecordType::kVirtualCard |
| ? PaymentsRpcCardType::kVirtualCard |
| : PaymentsRpcCardType::kServerCard); |
| } |
| |
| // If the to-be-unmasked card is a virtual card eligible card (this also |
| // implicitly checks the RecordType to be masked server card), send the |
| // virtual card enrollment preflight call early. |
| auto* virtual_card_enrollment_manager = |
| payments_autofill_client().GetVirtualCardEnrollmentManager(); |
| if (card->virtual_card_enrollment_state() == |
| CreditCard::VirtualCardEnrollmentState::kUnenrolledAndEligible && |
| virtual_card_enrollment_manager && |
| base::FeatureList::IsEnabled( |
| features:: |
| kAutofillEnableMultipleRequestInVirtualCardDownstreamEnrollment)) { |
| // Set empty callback as we need to wait for form submission & card |
| // extraction from the form, before we start the next step. |
| virtual_card_enrollment_manager->InitVirtualCardEnroll( |
| *card, VirtualCardEnrollmentSource::kDownstream, |
| /*virtual_card_enrollment_fields_loaded_callback=*/base::DoNothing()); |
| } |
| |
| // If card has been previously unmasked, use cached data. |
| std::unordered_map<std::string, CachedServerCardInfo>::iterator it = |
| unmasked_card_cache_.find(GetKeyForUnmaskedCardsCache(*card)); |
| if (it != unmasked_card_cache_.end()) { // key is in cache |
| it->second.card.set_cvc(it->second.cvc); |
| OnCreditCardFetched(it->second.card, /*card_was_fetched_from_cache=*/true); |
| |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kLocalCacheHit, record_type, |
| ServerCardUnmaskFlowType::kUnspecified); |
| |
| Reset(); |
| return; |
| } |
| |
| card_ = std::make_unique<CreditCard>(*card); |
| switch (record_type) { |
| case CreditCard::RecordType::kVirtualCard: |
| return FetchVirtualCard(); |
| case CreditCard::RecordType::kMaskedServerCard: |
| return FetchMaskedServerCard(); |
| case CreditCard::RecordType::kLocalCard: |
| return FetchLocalCard(); |
| case CreditCard::RecordType::kFullServerCard: |
| // Full server cards are only possible after a previous unmasking on the |
| // same page. When that happens, subsequent repeat fills of the unmasked |
| // card should flow through the cache and not reach here. |
| NOTREACHED(); |
| } |
| } |
| |
| bool CreditCardAccessManager::IsMaskedServerCardRiskBasedAuthAvailable() const { |
| // On some particular platforms (iOS WebView i.e.), Hagrid (risk based |
| // authentication) is not supported. This check if the current platform |
| // supports Hagrid. |
| if (!payments_autofill_client().IsRiskBasedAuthEffectivelyAvailable()) { |
| return false; |
| } |
| |
| bool isCardInfoRetrievalEnrolled = |
| base::FeatureList::IsEnabled( |
| features::kAutofillEnableCardInfoRuntimeRetrieval) && |
| (card_->card_info_retrieval_enrollment_state() == |
| CreditCard::CardInfoRetrievalEnrollmentState::kRetrievalEnrolled); |
| return !card_->IsExpired(AutofillClock::Now()) && |
| (base::FeatureList::IsEnabled( |
| features::kAutofillEnableFpanRiskBasedAuthentication) || |
| isCardInfoRetrievalEnrolled); |
| } |
| |
| void CreditCardAccessManager::FIDOAuthOptChange(bool opt_in) { |
| #if BUILDFLAG(IS_IOS) |
| return; |
| #else |
| if (opt_in) { |
| ShowWebauthnOfferDialog(/*card_authorization_token=*/std::string()); |
| } else { |
| // We should not offer to update any user preferences when the user is off |
| // the record. This also protects against a possible crash when attempting |
| // to add the maximum amount of strikes to the FIDO auth strike database, as |
| // strike databases are not present in incognito mode and should not be |
| // used. |
| if (autofill_client().IsOffTheRecord()) { |
| return; |
| } |
| |
| GetOrCreateFidoAuthenticator()->OptOut(); |
| if (auto* strike_database = |
| GetOrCreateFidoAuthenticator() |
| ->GetOrCreateFidoAuthenticationStrikeDatabase()) { |
| strike_database->AddStrikes( |
| FidoAuthenticationStrikeDatabase::kStrikesToAddWhenUserOptsOut); |
| } |
| } |
| #endif |
| } |
| |
| void CreditCardAccessManager::OnSettingsPageFIDOAuthToggled(bool opt_in) { |
| #if BUILDFLAG(IS_IOS) |
| return; |
| #else |
| // TODO(crbug.com/40621544): Add a rate limiter to counter spam clicking. |
| FIDOAuthOptChange(opt_in); |
| #endif |
| } |
| |
| void CreditCardAccessManager::SignalCanFetchUnmaskDetails() { |
| can_fetch_unmask_details_ = true; |
| } |
| |
| void CreditCardAccessManager::CacheUnmaskedCardInfo(const CreditCard& card, |
| const std::u16string& cvc) { |
| DCHECK(card.record_type() == CreditCard::RecordType::kFullServerCard || |
| card.record_type() == CreditCard::RecordType::kVirtualCard); |
| std::string identifier = |
| card.record_type() == CreditCard::RecordType::kVirtualCard |
| ? card.server_id() + kVirtualCardIdentifier |
| : card.server_id(); |
| CachedServerCardInfo card_info = {card, cvc, /*cache_uses=*/0}; |
| unmasked_card_cache_[identifier] = card_info; |
| } |
| |
| void CreditCardAccessManager::StartAuthenticationFlow(bool fido_auth_enabled) { |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| StartAuthenticationFlowForVirtualCard(fido_auth_enabled); |
| } else { |
| StartAuthenticationFlowForMaskedServerCard(fido_auth_enabled); |
| } |
| } |
| |
| void CreditCardAccessManager::StartAuthenticationFlowForVirtualCard( |
| bool fido_auth_enabled) { |
| // TODO(crbug.com/40195445): Currently if the card is a virtual card and FIDO |
| // auth was provided by issuer, we prefer FIDO auth. Remove FIDO preference |
| // and allow user selections later. |
| if (fido_auth_enabled) { |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| ShowVerifyPendingDialog(); |
| #endif |
| Authenticate(UnmaskAuthFlowType::kFido); |
| return; |
| } |
| |
| // Otherwise, we first check if other options are provided. If not, end the |
| // session and display an error dialog. |
| std::vector<CardUnmaskChallengeOption>& challenge_options = |
| risk_based_authentication_response_.card_unmask_challenge_options; |
| if (challenge_options.empty()) { |
| payments_autofill_client().ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/true)); |
| Reset(); |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kOnlyFidoAvailableButNotOptedIn, |
| PaymentsRpcCardType::kVirtualCard, ServerCardUnmaskFlowType::kFidoOnly); |
| return; |
| } |
| |
| // If we only have one challenge option, and it is a CVC challenge option, go |
| // directly to the CVC input dialog. |
| if (challenge_options.size() == 1 && |
| challenge_options[0].type == CardUnmaskChallengeOptionType::kCvc) { |
| selected_challenge_option_ = &challenge_options[0]; |
| Authenticate(UnmaskAuthFlowType::kCvc); |
| return; |
| } |
| |
| // If we only have one challenge option, and it is a 3DS challenge option, |
| // authenticate as kThreeDomainSecure flow type. |
| if (challenge_options.size() == 1 && |
| challenge_options[0].type == |
| CardUnmaskChallengeOptionType::kThreeDomainSecure) { |
| selected_challenge_option_ = &challenge_options[0]; |
| Authenticate(UnmaskAuthFlowType::kThreeDomainSecure); |
| return; |
| } |
| |
| // If we have multiple challenge options available, render the challenge |
| // option selection dialog. This dialog also handles the case where we only |
| // have an OTP challenge option. |
| ShowUnmaskAuthenticatorSelectionDialog(); |
| } |
| |
| void CreditCardAccessManager::StartAuthenticationFlowForMaskedServerCard( |
| bool fido_auth_enabled) { |
| // We check if the card is enrolled in runtime retrieval and only SMS OTP |
| // challenge options are present, then render the challenge option selection |
| // dialog. Currently the selection dialog box is only supported for SMS OTP |
| // challenges for masked server cards. |
| if (IsEligibleForCardInfoRetrievalAuthentication( |
| *card_, |
| risk_based_authentication_response_.card_unmask_challenge_options)) { |
| ShowUnmaskAuthenticatorSelectionDialog(); |
| return; |
| } |
| |
| UnmaskAuthFlowType flow_type; |
| #if BUILDFLAG(IS_IOS) |
| // On iOS only the CVC auth is available for masked server card. |
| flow_type = UnmaskAuthFlowType::kCvc; |
| #else |
| // If not enrolled in runtime retrieval then currently only FIDO and CVC auth |
| // are available for masked server card. |
| if (!fido_auth_enabled) { |
| // If FIDO auth is not enabled we offer CVC auth. |
| flow_type = UnmaskAuthFlowType::kCvc; |
| } else if (!IsSelectedCardFidoAuthorized()) { |
| // If FIDO auth is enabled but the card has not been authorized for FIDO, we |
| // offer CVC auth followed with a FIDO authorization. |
| flow_type = UnmaskAuthFlowType::kCvcThenFido; |
| } else if (!card_->IsExpired(AutofillClock::Now())) { |
| // If the FIDO auth is enabled and card has been authorized and card is not |
| // expired, we offer FIDO auth. |
| flow_type = UnmaskAuthFlowType::kFido; |
| } else { |
| // For other cases we offer CVC auth as well. E.g. A card that has been |
| // authorized but is expired. |
| flow_type = UnmaskAuthFlowType::kCvc; |
| } |
| #endif |
| |
| Authenticate(flow_type); |
| } |
| |
| void CreditCardAccessManager::Authenticate( |
| UnmaskAuthFlowType unmask_auth_flow_type) { |
| unmask_auth_flow_type_ = unmask_auth_flow_type; |
| |
| // Reset now that we have started authentication. |
| ready_to_start_authentication_.Reset(); |
| unmask_details_request_in_progress_ = false; |
| |
| form_event_logger_->LogCardUnmaskAuthenticationPromptShown( |
| unmask_auth_flow_type_); |
| |
| // If FIDO auth was suggested, log which authentication method was |
| // actually used. |
| switch (unmask_auth_flow_type_) { |
| case UnmaskAuthFlowType::kFido: { |
| autofill_metrics::LogCardUnmaskTypeDecision( |
| autofill_metrics::CardUnmaskTypeDecisionMetric::kFidoOnly); |
| #if BUILDFLAG(IS_IOS) |
| NOTREACHED(); |
| #else |
| // If |is_authentication_in_progress_| is false, it means the process has |
| // been cancelled via the verification pending dialog. Do not run |
| // CreditCardFidoAuthenticator::Authenticate() in this case (should not |
| // fall back to CVC auth either). |
| if (!is_authentication_in_progress_) { |
| Reset(); |
| return; |
| } |
| |
| // For virtual cards and masked server cards, the `fido_request_options` |
| // come from the risk based authentication response. But for masked server |
| // cards if the risk based auth is not available, `fido_request_options` |
| // comes from the UnmaskDetails. |
| base::Value::Dict fido_request_options; |
| std::optional<std::string> context_token; |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| context_token = risk_based_authentication_response_.context_token; |
| fido_request_options = |
| std::move(risk_based_authentication_response_.fido_request_options); |
| } else { |
| CHECK_EQ(card_->record_type(), |
| CreditCard::RecordType::kMaskedServerCard); |
| if (IsMaskedServerCardRiskBasedAuthAvailable()) { |
| // If risk-based authentication is available, `context_token` is |
| // needed for further authentication. `fido_request_options` is |
| // returned within the risk-based authentication response. |
| CHECK(!risk_based_authentication_response_.context_token.empty()); |
| context_token = risk_based_authentication_response_.context_token; |
| fido_request_options = std::move( |
| risk_based_authentication_response_.fido_request_options); |
| } else { |
| // If risk-based authentication is not available, the response of |
| // UnmaskDetails preflight call will be used as the resource of |
| // `fido_request_options`. |
| fido_request_options = |
| std::move(unmask_details_.fido_request_options); |
| } |
| } |
| GetOrCreateFidoAuthenticator()->Authenticate( |
| *card_, GetWeakPtr(), std::move(fido_request_options), context_token); |
| break; |
| #endif |
| } |
| case UnmaskAuthFlowType::kCvcThenFido: |
| autofill_metrics::LogCardUnmaskTypeDecision( |
| autofill_metrics::CardUnmaskTypeDecisionMetric::kCvcThenFido); |
| [[fallthrough]]; |
| case UnmaskAuthFlowType::kCvc: |
| case UnmaskAuthFlowType::kCvcFallbackFromFido: { |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| // Close the Webauthn verify pending dialog if it enters CVC |
| // authentication flow since the card unmask prompt will pop up. |
| payments_autofill_client().CloseWebauthnDialog(); |
| #endif |
| |
| // Delegate the task to CreditCardCvcAuthenticator. |
| // If we are in the virtual card CVC auth case, we must also pass in |
| // the vcn context token and the selected challenge option. |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| DCHECK(selected_challenge_option_); |
| payments_autofill_client().GetCvcAuthenticator().Authenticate( |
| *card_, GetWeakPtr(), |
| risk_based_authentication_response_.context_token, |
| *selected_challenge_option_); |
| } else if (IsMaskedServerCardRiskBasedAuthAvailable()) { |
| CHECK(!risk_based_authentication_response_.context_token.empty()); |
| payments_autofill_client().GetCvcAuthenticator().Authenticate( |
| *card_, GetWeakPtr(), |
| risk_based_authentication_response_.context_token); |
| } else { |
| payments_autofill_client().GetCvcAuthenticator().Authenticate( |
| *card_, GetWeakPtr()); |
| } |
| break; |
| } |
| case UnmaskAuthFlowType::kOtp: |
| case UnmaskAuthFlowType::kOtpFallbackFromFido: { |
| CHECK(card_->record_type() == CreditCard::RecordType::kVirtualCard || |
| card_->record_type() == CreditCard::RecordType::kMaskedServerCard); |
| // Delegate the task to CreditCardOtpAuthenticator. |
| DCHECK(selected_challenge_option_); |
| payments_autofill_client() |
| .GetOtpAuthenticator() |
| ->OnChallengeOptionSelected( |
| *card_, *selected_challenge_option_, GetWeakPtr(), |
| risk_based_authentication_response_.context_token, |
| payments::GetBillingCustomerId(payments_data_manager())); |
| break; |
| } |
| case UnmaskAuthFlowType::kThreeDomainSecure: |
| case UnmaskAuthFlowType::kThreeDomainSecureConsentAlreadyGiven: { |
| CHECK(card_); |
| CHECK(selected_challenge_option_); |
| payments::PaymentsWindowManager::Vcn3dsContext vcn_3ds_context; |
| vcn_3ds_context.context_token = |
| risk_based_authentication_response_.context_token; |
| vcn_3ds_context.card = *card_; |
| vcn_3ds_context.challenge_option = *selected_challenge_option_; |
| vcn_3ds_context.completion_callback = base::BindOnce( |
| &CreditCardAccessManager::OnVcn3dsAuthenticationComplete, |
| GetWeakPtr()); |
| vcn_3ds_context.user_consent_already_given = |
| unmask_auth_flow_type_ == |
| UnmaskAuthFlowType::kThreeDomainSecureConsentAlreadyGiven; |
| payments_autofill_client() |
| .GetPaymentsWindowManager() |
| ->InitVcn3dsAuthentication(std::move(vcn_3ds_context)); |
| break; |
| } |
| case UnmaskAuthFlowType::kNone: |
| NOTREACHED(); |
| } |
| } |
| |
| #if !BUILDFLAG(IS_IOS) |
| CreditCardFidoAuthenticator* |
| CreditCardAccessManager::GetOrCreateFidoAuthenticator() { |
| if (!fido_authenticator_) { |
| fido_authenticator_ = std::make_unique<CreditCardFidoAuthenticator>( |
| &manager_->driver(), &autofill_client()); |
| } |
| return fido_authenticator_.get(); |
| } |
| #endif |
| |
| void CreditCardAccessManager::OnCvcAuthenticationComplete( |
| const CreditCardCvcAuthenticator::CvcAuthenticationResponse& response) { |
| is_authentication_in_progress_ = false; |
| can_fetch_unmask_details_ = true; |
| |
| // Save credit card for caching purpose. CVC is also saved if response |
| // contains CVC. `response.card` can be nullptr in the case of an error in the |
| // response. If the response has an error, the `ShouldRespondImmediately()` |
| // call below will return true and we will safely pass nullptr and that it is |
| // an error into the `OnCreditCardFetchedCallback`, and end the flow. |
| if (response.card) { |
| // TODO(crbug.com/40929439): Deprecate `response.cvc` and |
| // `response.card.cvc`. |
| card_ = std::make_unique<CreditCard>(*response.card); |
| card_->set_cvc(response.cvc); |
| } |
| |
| // Log completed CVC authentication if auth was successful. Do not log for |
| // kCvcThenFido flow since that is yet to be completed. |
| // TODO(crbug.com/40280410): Refactor log into the new |
| // LogMetricsAndFillFormForServerUnmaskFlows(). |
| if (response.did_succeed && |
| unmask_auth_flow_type_ != UnmaskAuthFlowType::kCvcThenFido) { |
| form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted( |
| unmask_auth_flow_type_); |
| } |
| |
| bool should_register_card_with_fido = ShouldRegisterCardWithFido(response); |
| if (ShouldRespondImmediately(response)) { |
| DCHECK(!should_register_card_with_fido); |
| if (response.did_succeed) { |
| OnCreditCardFetched(*card_, /*card_was_fetched_from_cache=*/false); |
| } |
| unmask_auth_flow_type_ = UnmaskAuthFlowType::kNone; |
| } else if (should_register_card_with_fido) { |
| #if !BUILDFLAG(IS_IOS) |
| base::Value::Dict request_options; |
| if (!unmask_details_.fido_request_options.empty()) { |
| // For opted-in user (CVC then FIDO case), request options are returned in |
| // unmask detail response. |
| request_options = unmask_details_.fido_request_options.Clone(); |
| } else if (!response.request_options.empty()) { |
| // For Android users, request_options are provided from GetRealPan if the |
| // user has chosen to opt-in. |
| request_options = response.request_options.Clone(); |
| } |
| |
| // Additionally authorizes the card with FIDO. It also delays the form |
| // filling. |
| GetOrCreateFidoAuthenticator()->Authorize(GetWeakPtr(), |
| response.card_authorization_token, |
| std::move(request_options)); |
| #endif |
| } |
| if (ShouldOfferFidoOptInDialog(response)) { |
| // CreditCardFidoAuthenticator will handle enrollment completely. |
| ShowWebauthnOfferDialog(response.card_authorization_token); |
| } |
| |
| HandleFidoOptInStatusChange(); |
| // TODO(crbug.com/40197696): Add Reset() to this function after cleaning up |
| // the FIDO opt-in status change. This should not have any negative impact now |
| // except for readability and cleanness. The result of |
| // ShouldOfferFidoOptInDialog() and |opt_in_intention_| are to some extent |
| // duplicate. We should be able to combine them into one function. |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| bool CreditCardAccessManager::ShouldOfferFidoAuth() const { |
| if (!unmask_details_.server_denotes_fido_eligible_but_not_opted_in && |
| !unmask_details_.fido_request_options.empty()) { |
| // Server instructed the client to not offer FIDO because the client is |
| // already opted in. This can be verified with the presence of request |
| // options in the server response. |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason::kAlreadyOptedIn); |
| return false; |
| } |
| |
| if (!unmask_details_.server_denotes_fido_eligible_but_not_opted_in) { |
| // If the server thinks FIDO opt-in is not required for this user, then we |
| // won't offer the FIDO opt-in checkbox on the card unmask dialog. Since the |
| // client is not opted-in and device is eligible, this could mean that the |
| // server does not have a valid key for this device or the server is in a |
| // bad state. |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason:: |
| kUnmaskDetailsOfferFidoOptInFalse); |
| return false; |
| } |
| |
| if (opt_in_intention_ == UserOptInIntention::kIntentToOptIn) { |
| // If the user opted-in through the settings page, do not show checkbox. |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason:: |
| kOptedInFromSettings); |
| return false; |
| } |
| |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| // We should not offer FIDO opt-in for virtual cards. |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason::kVirtualCard); |
| return false; |
| } |
| |
| // No situations were found where we should not show the checkbox, so we |
| // should return true to indicate that we should display the checkbox to the |
| // user. |
| return true; |
| } |
| |
| bool CreditCardAccessManager::UserOptedInToFidoFromSettingsPageOnMobile() |
| const { |
| return opt_in_intention_ == UserOptInIntention::kIntentToOptIn; |
| } |
| #endif |
| |
| #if !BUILDFLAG(IS_IOS) |
| void CreditCardAccessManager::OnFIDOAuthenticationComplete( |
| const CreditCardFidoAuthenticator::FidoAuthenticationResponse& response) { |
| #if !BUILDFLAG(IS_ANDROID) |
| // Close the Webauthn verify pending dialog. If FIDO authentication succeeded, |
| // card is filled to the form, otherwise fall back to CVC authentication which |
| // does not need the verify pending dialog either. |
| payments_autofill_client().CloseWebauthnDialog(); |
| #endif |
| |
| if (response.did_succeed) { |
| // Save credit card for caching purpose. |
| if (response.card) { |
| card_ = std::make_unique<CreditCard>(*response.card); |
| } |
| LogMetricsAndFillFormForServerUnmaskFlows(unmask_auth_flow_type_); |
| Reset(); |
| } else if ( |
| response.failure_type == |
| payments::FullCardRequest::VIRTUAL_CARD_RETRIEVAL_TRANSIENT_FAILURE || |
| response.failure_type == |
| payments::FullCardRequest::VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE) { |
| // If it is an virtual card retrieval error, we don't want to invoke the CVC |
| // authentication afterwards. Instead reset all states and invoke the error |
| // dialog. |
| payments_autofill_client().ShowAutofillErrorDialog( |
| AutofillErrorDialogContext::WithVirtualCardPermanentOrTemporaryError( |
| /*is_permanent_error=*/response.failure_type == |
| payments::FullCardRequest:: |
| VIRTUAL_CARD_RETRIEVAL_PERMANENT_FAILURE)); |
| |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kVirtualCardRetrievalError, |
| PaymentsRpcCardType::kVirtualCard, |
| ServerCardUnmaskFlowType::kFidoOnly); |
| } |
| Reset(); |
| } else { |
| // If it is an authentication error, start the CVC authentication process |
| // for masked server cards or the virtual card authentication process for |
| // virtual cards. |
| if (card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| StartAuthenticationFlowForVirtualCard(/*fido_auth_enabled=*/false); |
| } else { |
| Authenticate(UnmaskAuthFlowType::kCvcFallbackFromFido); |
| } |
| } |
| } |
| |
| void CreditCardAccessManager::OnFidoAuthorizationComplete(bool did_succeed) { |
| if (did_succeed) { |
| OnCreditCardFetched(*card_, /*card_was_fetched_from_cache=*/false); |
| form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted( |
| unmask_auth_flow_type_); |
| } |
| Reset(); |
| } |
| #endif |
| |
| void CreditCardAccessManager::OnOtpAuthenticationComplete( |
| const OtpAuthenticationResponse& response) { |
| // Save credit card for caching purpose. CVC is also saved if response |
| // contains CVC. `response.card` can be nullptr in the case of an error in the |
| // response. If the response has an error, we will safely pass nullptr and |
| // that it is an error into the `OnCreditCardFetchedCallback` and end the |
| // flow. |
| if (response.card) { |
| card_ = std::make_unique<CreditCard>(*response.card); |
| card_->set_cvc(response.cvc); |
| } |
| if (response.result == OtpAuthenticationResponse::Result::kSuccess) { |
| // Authentication succeeded, log metrics and fill the form. |
| LogMetricsAndFillFormForServerUnmaskFlows(unmask_auth_flow_type_); |
| } else { |
| // Authentication failed, log the proper error metrics. |
| ServerCardUnmaskResult server_card_unmask_result = |
| ServerCardUnmaskResult::kUnknown; |
| CardInfoRetrievalEnrolledUnmaskResult |
| card_info_retrieval_enrolled_card_unmask_result = |
| CardInfoRetrievalEnrolledUnmaskResult::kUnknown; |
| switch (response.result) { |
| case OtpAuthenticationResponse::Result::kFlowCancelled: |
| server_card_unmask_result = ServerCardUnmaskResult::kFlowCancelled; |
| card_info_retrieval_enrolled_card_unmask_result = |
| CardInfoRetrievalEnrolledUnmaskResult::kFlowCancelled; |
| break; |
| case OtpAuthenticationResponse::Result::kGenericError: |
| server_card_unmask_result = ServerCardUnmaskResult::kUnexpectedError; |
| card_info_retrieval_enrolled_card_unmask_result = |
| CardInfoRetrievalEnrolledUnmaskResult::kUnexpectedError; |
| break; |
| case OtpAuthenticationResponse::Result::kAuthenticationError: |
| server_card_unmask_result = |
| ServerCardUnmaskResult::kAuthenticationError; |
| card_info_retrieval_enrolled_card_unmask_result = |
| CardInfoRetrievalEnrolledUnmaskResult::kAuthenticationError; |
| break; |
| case OtpAuthenticationResponse::Result::kVirtualCardRetrievalError: |
| server_card_unmask_result = |
| ServerCardUnmaskResult::kVirtualCardRetrievalError; |
| break; |
| case OtpAuthenticationResponse::Result::kSuccess: |
| case OtpAuthenticationResponse::Result::kUnknown: |
| NOTREACHED(); |
| } |
| |
| ServerCardUnmaskFlowType flow_type; |
| if (unmask_auth_flow_type_ == UnmaskAuthFlowType::kOtp) { |
| flow_type = ServerCardUnmaskFlowType::kOtpOnly; |
| } else { |
| DCHECK(unmask_auth_flow_type_ == |
| UnmaskAuthFlowType::kOtpFallbackFromFido); |
| flow_type = ServerCardUnmaskFlowType::kOtpFallbackFromFido; |
| } |
| autofill_metrics::LogServerCardUnmaskResult( |
| server_card_unmask_result, PaymentsRpcCardType::kVirtualCard, |
| flow_type); |
| if (card_->IsEnrolledInCardInfoRetrieval()) { |
| autofill_metrics::LogCardInfoRetrievalEnrolledUnmaskResult( |
| card_info_retrieval_enrolled_card_unmask_result); |
| } |
| } |
| |
| HandleFidoOptInStatusChange(); |
| Reset(); |
| } |
| |
| bool CreditCardAccessManager::IsUserOptedInToFidoAuth() { |
| #if BUILDFLAG(IS_IOS) |
| return false; |
| #else |
| return is_user_verifiable_.value_or(false) && |
| GetOrCreateFidoAuthenticator()->IsUserOptedIn(); |
| #endif |
| } |
| |
| bool CreditCardAccessManager::IsFidoAuthEnabled(bool fido_auth_offered) { |
| // FIDO auth is enabled if payments offers FIDO auth, and local pref |
| // indicates that the user is opted-in. |
| return fido_auth_offered && IsUserOptedInToFidoAuth(); |
| } |
| |
| bool CreditCardAccessManager::IsSelectedCardFidoAuthorized() { |
| // The selected card is not FIDO authorized if the user has not opted in to |
| // FIDO auth. |
| if (!IsUserOptedInToFidoAuth()) { |
| return false; |
| } |
| // If risk-based authentication is available, `fido_request_options` returned |
| // by the risk-based authentication call will be used as the indicator about |
| // whether the selected card is FIDO authorized. |
| if (IsMaskedServerCardRiskBasedAuthAvailable()) { |
| return !risk_based_authentication_response_.fido_request_options.empty(); |
| } |
| DCHECK_NE(unmask_details_.unmask_auth_method, |
| payments::PaymentsAutofillClient::UnmaskAuthMethod::kUnknown); |
| return unmask_details_.fido_eligible_card_ids.find(card_->server_id()) != |
| unmask_details_.fido_eligible_card_ids.end(); |
| } |
| |
| bool CreditCardAccessManager::ShouldRespondImmediately( |
| const CreditCardCvcAuthenticator::CvcAuthenticationResponse& response) { |
| #if BUILDFLAG(IS_ANDROID) |
| // GetRealPan did not return valid RequestOptions (user did not specify intent |
| // to opt-in or the server returned invalid RequestOptions) AND flow is not |
| // registering a new card, so fill the form directly. |
| if (!GetOrCreateFidoAuthenticator()->IsValidRequestOptions( |
| response.request_options) && |
| unmask_auth_flow_type_ != UnmaskAuthFlowType::kCvcThenFido) { |
| return true; |
| } |
| #else |
| if (unmask_auth_flow_type_ != UnmaskAuthFlowType::kCvcThenFido) { |
| return true; |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| #if !BUILDFLAG(IS_IOS) |
| // If the current flow is `kCvcThenFido` and there are no valid FIDO request |
| // options present, fill the form immediately, as FIDO registration is not |
| // possible. |
| if (unmask_auth_flow_type_ == UnmaskAuthFlowType::kCvcThenFido && |
| !GetOrCreateFidoAuthenticator()->IsValidRequestOptions( |
| unmask_details_.fido_request_options)) { |
| return true; |
| } |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| // If the response did not succeed, report the error immediately. If |
| // GetRealPan did not return a card authorization token (we can't call any |
| // FIDO-related flows, either opt-in or register new card, without the token), |
| // fill the form immediately. |
| return !response.did_succeed || response.card_authorization_token.empty(); |
| } |
| |
| bool CreditCardAccessManager::ShouldRegisterCardWithFido( |
| const CreditCardCvcAuthenticator::CvcAuthenticationResponse& response) { |
| // Card authorization token is required in order to call |
| // CreditCardFidoAuthenticator::Authorize(), so if we do not have a card |
| // authorization token populated we immediately return false. |
| if (response.card_authorization_token.empty()) { |
| return false; |
| } |
| |
| #if !BUILDFLAG(IS_IOS) |
| // `unmask_auth_flow_type_` is kCvcThenFido, and there are valid FIDO request |
| // options present, so the user is already opted-in and the new card must |
| // additionally be authorized through WebAuthn. |
| if (unmask_auth_flow_type_ == UnmaskAuthFlowType::kCvcThenFido && |
| GetOrCreateFidoAuthenticator()->IsValidRequestOptions( |
| unmask_details_.fido_request_options)) { |
| return true; |
| } |
| #endif |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // For Android, we will delay the form filling for both intent-to-opt-in user |
| // opting in and opted-in user registering a new card (kCvcThenFido). So we |
| // check one more scenario for Android here. If the GetRealPan response |
| // includes valid `request_options`, that means the user showed intention to |
| // opt-in while unmasking and must complete the challenge before successfully |
| // opting-in and filling the form. |
| if (GetOrCreateFidoAuthenticator()->IsValidRequestOptions( |
| response.request_options)) { |
| return true; |
| } |
| #endif |
| |
| // No conditions to offer FIDO registration are met, so we return false. |
| return false; |
| } |
| |
| bool CreditCardAccessManager::ShouldOfferFidoOptInDialog( |
| const CreditCardCvcAuthenticator::CvcAuthenticationResponse& response) { |
| #if BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID) |
| // We should not offer FIDO opt-in dialog on mobile. |
| return false; |
| #else |
| if (!unmask_details_.server_denotes_fido_eligible_but_not_opted_in && |
| !unmask_details_.fido_request_options.empty()) { |
| // Server instructed the client to not offer FIDO because the client is |
| // already opted in. This can be verified with the presence of request |
| // options in the server response. |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason::kAlreadyOptedIn); |
| return false; |
| } |
| |
| // If this card is not eligible for offering FIDO opt-in, we should not offer |
| // the FIDO opt-in dialog. Since the client is not opted-in and device is |
| // eligible, this could mean that the server does not have a valid key for |
| // this device or the server is in a bad state. |
| if (!unmask_details_.server_denotes_fido_eligible_but_not_opted_in) { |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason:: |
| kUnmaskDetailsOfferFidoOptInFalse); |
| return false; |
| } |
| |
| // A card authorization token is required for FIDO opt-in, so if we did not |
| // receive one from the server we should not offer the FIDO opt-in dialog. |
| if (response.card_authorization_token.empty()) { |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason:: |
| kCardAuthorizationTokenEmpty); |
| return false; |
| } |
| |
| if (auto* strike_database = |
| GetOrCreateFidoAuthenticator() |
| ->GetOrCreateFidoAuthenticationStrikeDatabase(); |
| strike_database && strike_database->ShouldBlockFeature()) { |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason:: |
| kBlockedByStrikeDatabase); |
| return false; |
| } |
| |
| // We should not offer FIDO opt-in for virtual cards. |
| if (!card_ || card_->record_type() == CreditCard::RecordType::kVirtualCard) { |
| autofill_metrics::LogWebauthnOptInPromoNotOfferedReason( |
| autofill_metrics::WebauthnOptInPromoNotOfferedReason::kVirtualCard); |
| return false; |
| } |
| |
| // None of the cases where we should not offer the FIDO opt-in dialog were |
| // true, so we should offer it. |
| return true; |
| #endif |
| } |
| |
| void CreditCardAccessManager::ShowWebauthnOfferDialog( |
| std::string card_authorization_token) { |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| GetOrCreateFidoAuthenticator()->OnWebauthnOfferDialogRequested( |
| card_authorization_token); |
| payments_autofill_client().ShowWebauthnOfferDialog(base::BindRepeating( |
| &CreditCardAccessManager::HandleDialogUserResponse, GetWeakPtr())); |
| #endif |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| void CreditCardAccessManager::ShowVerifyPendingDialog() { |
| payments_autofill_client().ShowWebauthnVerifyPendingDialog( |
| base::BindRepeating(&CreditCardAccessManager::HandleDialogUserResponse, |
| GetWeakPtr())); |
| } |
| |
| void CreditCardAccessManager::HandleDialogUserResponse( |
| WebauthnDialogCallbackType type) { |
| switch (type) { |
| case WebauthnDialogCallbackType::kOfferAccepted: |
| GetOrCreateFidoAuthenticator()->OnWebauthnOfferDialogUserResponse( |
| /*did_accept=*/true); |
| break; |
| case WebauthnDialogCallbackType::kOfferCancelled: |
| GetOrCreateFidoAuthenticator()->OnWebauthnOfferDialogUserResponse( |
| /*did_accept=*/false); |
| break; |
| case WebauthnDialogCallbackType::kVerificationCancelled: |
| // TODO(crbug.com/40621544): Add tests and logging for canceling verify |
| // pending dialog. |
| payments_autofill_client().GetPaymentsNetworkInterface()->CancelRequest(); |
| SignalCanFetchUnmaskDetails(); |
| ready_to_start_authentication_.Reset(); |
| unmask_details_request_in_progress_ = false; |
| GetOrCreateFidoAuthenticator()->CancelVerification(); |
| |
| // Indicate that FIDO authentication was canceled, resulting in falling |
| // back to CVC auth. |
| CreditCardFidoAuthenticator::FidoAuthenticationResponse response{ |
| .did_succeed = false}; |
| OnFIDOAuthenticationComplete(response); |
| break; |
| } |
| } |
| #endif |
| |
| std::string CreditCardAccessManager::GetKeyForUnmaskedCardsCache( |
| const CreditCard& card) const { |
| std::string key = card.server_id(); |
| if (card.record_type() == CreditCard::RecordType::kVirtualCard) { |
| key += kVirtualCardIdentifier; |
| } |
| return key; |
| } |
| |
| void CreditCardAccessManager::FetchMaskedServerCard() { |
| is_authentication_in_progress_ = true; |
| |
| bool get_unmask_details_returned = |
| ready_to_start_authentication_.IsSignaled(); |
| |
| if (IsMaskedServerCardRiskBasedAuthAvailable()) { |
| // Preflight call response time metrics should only be logged if the user is |
| // verifiable. |
| #if !BUILDFLAG(IS_IOS) |
| if (is_user_verifiable_.value_or(false)) { |
| autofill_metrics::LogPreflightCallResponseReceivedOnCardSelection( |
| get_unmask_details_returned |
| ? autofill_metrics::PreflightCallEvent:: |
| kPreflightCallReturnedBeforeCardChosen |
| : autofill_metrics::PreflightCallEvent:: |
| kCardChosenBeforePreflightCallReturned, |
| GetOrCreateFidoAuthenticator()->IsUserOptedIn(), |
| CreditCard::RecordType::kMaskedServerCard); |
| } |
| #endif |
| |
| payments_autofill_client().ShowAutofillProgressDialog( |
| card_->card_info_retrieval_enrollment_state() == |
| CreditCard::CardInfoRetrievalEnrollmentState::kRetrievalEnrolled |
| ? AutofillProgressDialogType:: |
| kCardInfoRetrievalEnrolledUnmaskProgressDialog |
| : AutofillProgressDialogType::kServerCardUnmaskProgressDialog, |
| /*cancel_callback=*/base::BindOnce( |
| &CreditCardRiskBasedAuthenticator::OnUnmaskCancelled, |
| payments_autofill_client() |
| .GetRiskBasedAuthenticator() |
| ->AsWeakPtr())); |
| |
| payments_autofill_client().GetRiskBasedAuthenticator()->Authenticate( |
| *card_, GetWeakPtr()); |
| // Risk-based authentication is handled in CreditCardRiskBasedAuthenticator. |
| // Further delegation will be handled in |
| // CreditCardAccessManager::OnRiskBasedAuthenticationResponseReceived. |
| return; |
| } |
| |
| // Latency metrics should only be logged if the user is verifiable. |
| #if !BUILDFLAG(IS_IOS) |
| if (is_user_verifiable_.value_or(false)) { |
| autofill_metrics::LogUserPerceivedLatencyOnCardSelection( |
| get_unmask_details_returned |
| ? autofill_metrics::PreflightCallEvent:: |
| kPreflightCallReturnedBeforeCardChosen |
| : autofill_metrics::PreflightCallEvent:: |
| kCardChosenBeforePreflightCallReturned, |
| GetOrCreateFidoAuthenticator()->IsUserOptedIn()); |
| } |
| #endif |
| |
| #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) |
| // On desktop, show the verify pending dialog for opted-in user, unless it is |
| // already known that selected card requires CVC. |
| if (IsUserOptedInToFidoAuth() && |
| (!get_unmask_details_returned || IsSelectedCardFidoAuthorized())) { |
| ShowVerifyPendingDialog(); |
| } |
| #endif |
| |
| bool should_wait_to_authenticate = |
| IsUserOptedInToFidoAuth() && !get_unmask_details_returned; |
| |
| if (should_wait_to_authenticate) { |
| card_selected_without_unmask_details_timestamp_ = base::TimeTicks::Now(); |
| |
| // Wait for |ready_to_start_authentication_| to be signaled by |
| // OnDidGetUnmaskDetails() or until timeout before calling |
| // OnStopWaitingForUnmaskDetails(). |
| ready_to_start_authentication_.OnEventOrTimeOut( |
| base::BindOnce(&CreditCardAccessManager::OnStopWaitingForUnmaskDetails, |
| GetWeakPtr()), |
| kUnmaskDetailsResponseTimeout); |
| } else { |
| StartAuthenticationFlow(IsFidoAuthEnabled( |
| get_unmask_details_returned && |
| unmask_details_.unmask_auth_method == |
| payments::PaymentsAutofillClient::UnmaskAuthMethod::kFido)); |
| } |
| } |
| |
| void CreditCardAccessManager::FetchVirtualCard() { |
| is_authentication_in_progress_ = true; |
| payments_autofill_client().ShowAutofillProgressDialog( |
| AutofillProgressDialogType::kVirtualCardUnmaskProgressDialog, |
| base::BindOnce(&CreditCardAccessManager::OnVirtualCardUnmaskCancelled, |
| GetWeakPtr())); |
| payments_autofill_client().GetRiskBasedAuthenticator()->Authenticate( |
| *card_, GetWeakPtr()); |
| } |
| |
| void CreditCardAccessManager::FetchLocalCard() { |
| CHECK_EQ(card_->record_type(), CreditCard::RecordType::kLocalCard); |
| |
| #if !BUILDFLAG(IS_IOS) |
| // Latency metrics should only be logged if the user is verifiable. |
| if (is_user_verifiable_.value_or(false)) { |
| autofill_metrics::LogUserPerceivedLatencyOnCardSelection( |
| autofill_metrics::PreflightCallEvent::kDidNotChooseMaskedCard, |
| GetOrCreateFidoAuthenticator()->IsUserOptedIn()); |
| } |
| #endif |
| |
| // Check if we need to authenticate the user before filling the local card. |
| if (payments_data_manager().IsPaymentMethodsMandatoryReauthEnabled()) { |
| // `StartDeviceAuthenticationForFilling()` will asynchronously trigger |
| // the re-authentication flow, so we should avoid calling `Reset()` |
| // until the re-authentication flow is complete. |
| StartDeviceAuthenticationForFilling(card_.get()); |
| } else { |
| if (!card_->cvc().empty()) { |
| autofill_metrics::LogCvcFilling( |
| autofill_metrics::CvcFillingFlowType::kNoInteractiveAuthentication, |
| CreditCard::RecordType::kLocalCard); |
| } |
| // Fill immediately if local card, as we do not need to authenticate the |
| // user. |
| OnCreditCardFetched(*card_, /*card_was_fetched_from_cache=*/false); |
| |
| // This local card autofill flow did not have any interactive |
| // authentication, so notify the FormDataImporter of this. |
| autofill_client() |
| .GetFormDataImporter() |
| ->SetPaymentMethodTypeIfNonInteractiveAuthenticationFlowCompleted( |
| payments::MandatoryReauthManager:: |
| GetNonInteractivePaymentMethodType( |
| CreditCard::RecordType::kLocalCard)); |
| |
| // `OnCreditCardFetchedCallback` makes a copy of `card` and `cvc` before it |
| // asynchronously fills them into the form. Thus we can safely call |
| // `Reset()` here, and we should as from this class' point of view the |
| // authentication flow is complete. |
| Reset(); |
| } |
| } |
| |
| void CreditCardAccessManager::OnRiskBasedAuthenticationResponseReceived( |
| const CreditCardRiskBasedAuthenticator::RiskBasedAuthenticationResponse& |
| response) { |
| selected_challenge_option_ = nullptr; |
| switch (response.result) { |
| case Result::kNoAuthenticationRequired: |
| // If the response indicates no further authentication is required, then |
| // complete card information has been fetched from the server (this is |
| // ensured in CreditCardRiskBasedAuthenticator). |
| CHECK(response.card.has_value()); |
| card_ = std::make_unique<CreditCard>(response.card.value()); |
| // When the card being retrieved is a masked server card, the |
| // `record_type` was set to kFullServerCard in the |
| // CreditCardRiskBasedAuthenticator due to the fact that masked server |
| // cards are treated as full server cards after unmasking. Hence we need |
| // to explicitly set `record type` to kMaskedServerCard for non virtual |
| // cards. |
| OnNonInteractiveAuthenticationSuccess( |
| response.card->record_type() == CreditCard::RecordType::kVirtualCard |
| ? CreditCard::RecordType::kVirtualCard |
| : CreditCard::RecordType::kMaskedServerCard); |
| break; |
| case Result::kAuthenticationRequired: |
| // Authenticates users to unmask the card if the response indicates |
| // further authentication is required. |
| payments_autofill_client().CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/false, |
| /*no_interactive_authentication_callback=*/base::OnceClosure()); |
| CHECK(!response.context_token.empty()); |
| risk_based_authentication_response_ = response; |
| // If `fido_request_options` is present, FIDO auth is offered to the card |
| // being retrieved. Otherwise, we need to check the returned value of |
| // GetUnmaskDetails to determine whether the card can be enrolled into |
| // FIDO. |
| StartAuthenticationFlow(IsFidoAuthEnabled( |
| /*fido_auth_offered=*/!response.fido_request_options.empty() || |
| // FIDO enrollment is not supported for virtual cards, so skip |
| // checking `unmask_details_.unmask_auth_method`, which is used for |
| // FIDO enrollment, in the virtual card case. |
| (card_->record_type() != CreditCard::RecordType::kVirtualCard && |
| unmask_details_.unmask_auth_method == |
| payments::PaymentsAutofillClient::UnmaskAuthMethod::kFido))); |
| break; |
| case Result::kAuthenticationCancelled: |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kFlowCancelled, card_->record_type(), |
| ServerCardUnmaskFlowType::kRiskBased); |
| if (card_->IsEnrolledInCardInfoRetrieval()) { |
| autofill_metrics::LogCardInfoRetrievalEnrolledUnmaskResult( |
| CardInfoRetrievalEnrolledUnmaskResult::kFlowCancelled); |
| } |
| Reset(); |
| break; |
| case Result::kError: |
| // Shows error dialog to users if the authentication failed. |
| payments_autofill_client().CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/false, |
| /*no_interactive_authentication_callback=*/base::OnceClosure()); |
| payments_autofill_client().ShowAutofillErrorDialog( |
| response.error_dialog_context); |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kAuthenticationError, |
| card_->record_type() == CreditCard::RecordType::kVirtualCard |
| ? PaymentsRpcCardType::kVirtualCard |
| : PaymentsRpcCardType::kServerCard, |
| ServerCardUnmaskFlowType::kRiskBased); |
| if (card_->IsEnrolledInCardInfoRetrieval()) { |
| autofill_metrics::LogCardInfoRetrievalEnrolledUnmaskResult( |
| CardInfoRetrievalEnrolledUnmaskResult::kAuthenticationError); |
| } |
| Reset(); |
| break; |
| case Result::kVirtualCardRetrievalError: |
| // Shows error dialog to users if the authentication failed. |
| payments_autofill_client().CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/false, |
| /*no_interactive_authentication_callback=*/base::OnceClosure()); |
| payments_autofill_client().ShowAutofillErrorDialog( |
| response.error_dialog_context); |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kVirtualCardRetrievalError, |
| PaymentsRpcCardType::kVirtualCard, |
| ServerCardUnmaskFlowType::kRiskBased); |
| Reset(); |
| break; |
| case Result::kUnknown: |
| NOTREACHED(); |
| } |
| } |
| |
| void CreditCardAccessManager::OnNonInteractiveAuthenticationSuccess( |
| CreditCard::RecordType record_type) { |
| if (payments_data_manager().IsPaymentMethodsMandatoryReauthEnabled()) { |
| // On some operating systems (for example, macOS and Windows), the |
| // device authentication prompt freezes Chrome. Thus we can only trigger |
| // the prompt after the progress dialog has been closed, which we can do |
| // by using the `no_interactive_authentication_callback` parameter in |
| // `PaymentsAutofillClient::CloseAutofillProgressDialog()`. |
| payments_autofill_client().CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/false, |
| /*no_interactive_authentication_callback=*/base::BindOnce( |
| // `StartDeviceAuthenticationForFilling()` will asynchronously |
| // trigger the re-authentication flow, so we should avoid |
| // calling `Reset()` until the re-authentication flow is |
| // complete. |
| &CreditCardAccessManager::StartDeviceAuthenticationForFilling, |
| GetWeakPtr(), card_.get())); |
| } else { |
| payments_autofill_client().CloseAutofillProgressDialog( |
| /*show_confirmation_before_closing=*/true, |
| /*no_interactive_authentication_callback=*/base::OnceClosure()); |
| OnCreditCardFetched(*card_, /*card_was_fetched_from_cache=*/false); |
| |
| // If the server returned a successful response along with the card's |
| // real PAN without requiring interactive authentication, set the |
| // `card_record_type_if_non_interactive_authentication_flow_completed_` |
| // field in FormDataImporter so that MandatoryReauthManager can decide |
| // whether to offer mandatory re-auth opt-in for this user. |
| autofill_client() |
| .GetFormDataImporter() |
| ->SetPaymentMethodTypeIfNonInteractiveAuthenticationFlowCompleted( |
| payments::MandatoryReauthManager:: |
| GetNonInteractivePaymentMethodType(record_type)); |
| |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kRiskBasedUnmasked, |
| record_type == CreditCard::RecordType::kVirtualCard |
| ? PaymentsRpcCardType::kVirtualCard |
| : PaymentsRpcCardType::kServerCard, |
| ServerCardUnmaskFlowType::kRiskBased); |
| |
| if (card_->card_info_retrieval_enrollment_state() == |
| CreditCard::CardInfoRetrievalEnrollmentState::kRetrievalEnrolled) { |
| autofill_metrics::LogCardInfoRetrievalEnrolledUnmaskResult( |
| CardInfoRetrievalEnrolledUnmaskResult::kRiskBasedUnmasked); |
| } |
| |
| if (!card_->cvc().empty()) { |
| autofill_metrics::LogCvcFilling( |
| autofill_metrics::CvcFillingFlowType::kNoInteractiveAuthentication, |
| record_type); |
| } |
| |
| // `OnCreditCardFetchedCallback` makes a copy of `card` and `cvc` before it |
| // asynchronously fills them into the form. Thus we can safely call |
| // `Reset()` here, and we should as from this class' point of view the |
| // authentication flow is complete. |
| Reset(); |
| } |
| } |
| |
| void CreditCardAccessManager::OnStopWaitingForUnmaskDetails( |
| bool get_unmask_details_returned) { |
| // If the user had to wait for Unmask Details, log the latency. |
| if (card_selected_without_unmask_details_timestamp_.has_value()) { |
| autofill_metrics::LogUserPerceivedLatencyOnCardSelectionDuration( |
| base::TimeTicks::Now() - |
| card_selected_without_unmask_details_timestamp_.value()); |
| autofill_metrics::LogUserPerceivedLatencyOnCardSelectionTimedOut( |
| /*did_time_out=*/!get_unmask_details_returned); |
| card_selected_without_unmask_details_timestamp_ = std::nullopt; |
| } |
| |
| // Start the authentication after the wait ends. |
| StartAuthenticationFlow(IsFidoAuthEnabled( |
| get_unmask_details_returned && |
| unmask_details_.unmask_auth_method == |
| payments::PaymentsAutofillClient::UnmaskAuthMethod::kFido)); |
| } |
| |
| void CreditCardAccessManager::OnUserAcceptedAuthenticationSelectionDialog( |
| const std::string& selected_challenge_option_id) { |
| selected_challenge_option_ = |
| GetCardUnmaskChallengeOptionForChallengeId(selected_challenge_option_id); |
| |
| // This if-statement should never be entered. We will only be selecting |
| // challenge options that we passed into the controller from |
| // `risk_based_authentication_response_.card_unmask_challenge_options`, |
| // which is also the vector that we search for challenge options in. For the |
| // context token, the server is guaranteed to always return a VCN context |
| // token for the virtual card authentication flow. This if-statement is just |
| // here as a safety. |
| if (!selected_challenge_option_ || |
| risk_based_authentication_response_.context_token.empty()) { |
| NOTREACHED(); |
| } |
| |
| UnmaskAuthFlowType selected_authentication_type = UnmaskAuthFlowType::kNone; |
| switch (selected_challenge_option_->type) { |
| case CardUnmaskChallengeOptionType::kCvc: |
| selected_authentication_type = UnmaskAuthFlowType::kCvc; |
| break; |
| case CardUnmaskChallengeOptionType::kSmsOtp: |
| case CardUnmaskChallengeOptionType::kEmailOtp: |
| selected_authentication_type = |
| unmask_auth_flow_type_ == UnmaskAuthFlowType::kFido |
| ? UnmaskAuthFlowType::kOtpFallbackFromFido |
| : UnmaskAuthFlowType::kOtp; |
| break; |
| case CardUnmaskChallengeOptionType::kThreeDomainSecure: |
| selected_authentication_type = |
| UnmaskAuthFlowType::kThreeDomainSecureConsentAlreadyGiven; |
| break; |
| case CardUnmaskChallengeOptionType::kUnknownType: |
| NOTREACHED(); |
| } |
| Authenticate(selected_authentication_type); |
| } |
| |
| void CreditCardAccessManager::OnVirtualCardUnmaskCancelled() { |
| if (unmask_auth_flow_type_ == UnmaskAuthFlowType::kOtp || |
| unmask_auth_flow_type_ == UnmaskAuthFlowType::kOtpFallbackFromFido) { |
| // It is possible to have the user hit the cancel button during an in-flight |
| // Virtual Card Unmask request, so we need to reset the state of the |
| // CreditCardOtpAuthenticator as well to ensure the flow does not continue, |
| // as continuing the flow can cause a crash. |
| payments_autofill_client().GetOtpAuthenticator()->Reset(); |
| } |
| |
| ServerCardUnmaskFlowType flow_type; |
| switch (unmask_auth_flow_type_) { |
| case UnmaskAuthFlowType::kOtp: |
| flow_type = ServerCardUnmaskFlowType::kOtpOnly; |
| break; |
| case UnmaskAuthFlowType::kOtpFallbackFromFido: |
| flow_type = ServerCardUnmaskFlowType::kOtpFallbackFromFido; |
| break; |
| case UnmaskAuthFlowType::kNone: |
| flow_type = ServerCardUnmaskFlowType::kRiskBased; |
| break; |
| case UnmaskAuthFlowType::kFido: |
| case UnmaskAuthFlowType::kCvcThenFido: |
| case UnmaskAuthFlowType::kCvcFallbackFromFido: |
| DUMP_WILL_BE_NOTREACHED(); |
| [[fallthrough]]; |
| case UnmaskAuthFlowType::kThreeDomainSecure: |
| // TODO(crbug.com/40240970): Add a flow type for the kThreeDomainSecure |
| // flow for metrics. |
| case UnmaskAuthFlowType::kThreeDomainSecureConsentAlreadyGiven: |
| // TODO(crbug.com/40240970): Add a flow type for the |
| // kThreeDomainSecureConsentAlreadyGiven flow for metrics. |
| case UnmaskAuthFlowType::kCvc: |
| // TODO(crbug.com/40240970): Add a flow type for the CVC flow for metrics. |
| Reset(); |
| return; |
| } |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kFlowCancelled, PaymentsRpcCardType::kVirtualCard, |
| flow_type); |
| Reset(); |
| } |
| |
| void CreditCardAccessManager::Reset() { |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| unmask_auth_flow_type_ = UnmaskAuthFlowType::kNone; |
| is_authentication_in_progress_ = false; |
| preflight_call_timestamp_ = std::nullopt; |
| card_selected_without_unmask_details_timestamp_ = std::nullopt; |
| is_user_verifiable_called_timestamp_ = std::nullopt; |
| #if !BUILDFLAG(IS_IOS) |
| opt_in_intention_ = UserOptInIntention::kUnspecified; |
| #endif |
| unmask_details_ = payments::UnmaskDetails(); |
| selected_challenge_option_ = nullptr; |
| risk_based_authentication_response_ = |
| CreditCardRiskBasedAuthenticator::RiskBasedAuthenticationResponse(); |
| ready_to_start_authentication_.Reset(); |
| can_fetch_unmask_details_ = true; |
| card_.reset(); |
| unmask_details_request_in_progress_ = false; |
| } |
| |
| void CreditCardAccessManager::HandleFidoOptInStatusChange() { |
| #if !BUILDFLAG(IS_IOS) |
| // If user intended to opt out, we will opt user out after CVC/OTP auth |
| // completes (no matter it succeeded or failed). |
| if (opt_in_intention_ == UserOptInIntention::kIntentToOptOut) { |
| FIDOAuthOptChange(/*opt_in=*/false); |
| } |
| // Reset |opt_in_intention_| after the authentication completes. |
| opt_in_intention_ = UserOptInIntention::kUnspecified; |
| #endif |
| } |
| |
| void CreditCardAccessManager::ShowUnmaskAuthenticatorSelectionDialog() { |
| payments_autofill_client().ShowUnmaskAuthenticatorSelectionDialog( |
| risk_based_authentication_response_.card_unmask_challenge_options, |
| base::BindOnce( |
| &CreditCardAccessManager::OnUserAcceptedAuthenticationSelectionDialog, |
| GetWeakPtr()), |
| base::BindOnce(&CreditCardAccessManager::OnVirtualCardUnmaskCancelled, |
| GetWeakPtr())); |
| } |
| |
| CardUnmaskChallengeOption* |
| CreditCardAccessManager::GetCardUnmaskChallengeOptionForChallengeId( |
| const std::string& challenge_id) { |
| CreditCard::RecordType card_record_type = card_->record_type(); |
| CHECK(card_record_type == CreditCard::RecordType::kVirtualCard || |
| card_record_type == CreditCard::RecordType::kMaskedServerCard); |
| std::vector<CardUnmaskChallengeOption>& challenge_options = |
| risk_based_authentication_response_.card_unmask_challenge_options; |
| auto card_unmask_challenge_options_it = std::ranges::find( |
| challenge_options, |
| CardUnmaskChallengeOption::ChallengeOptionId(challenge_id), |
| &CardUnmaskChallengeOption::id); |
| return card_unmask_challenge_options_it != challenge_options.end() |
| ? &(*card_unmask_challenge_options_it) |
| : nullptr; |
| } |
| |
| bool CreditCardAccessManager::ShouldLogServerCardUnmaskAttemptMetrics( |
| CreditCard::RecordType record_type) { |
| // Full server cards exist only in a temporarily cached state and should never |
| // be attempted to be unmasked. |
| CHECK_NE(record_type, CreditCard::RecordType::kFullServerCard); |
| |
| return record_type == CreditCard::RecordType::kMaskedServerCard || |
| record_type == CreditCard::RecordType::kVirtualCard; |
| } |
| |
| void CreditCardAccessManager::StartDeviceAuthenticationForFilling( |
| const CreditCard* card) { |
| is_authentication_in_progress_ = true; |
| |
| payments::MandatoryReauthAuthenticationMethod authentication_method = |
| autofill_client() |
| .GetPaymentsAutofillClient() |
| ->GetOrCreatePaymentsMandatoryReauthManager() |
| ->GetAuthenticationMethod(); |
| |
| // If there is no supported auth method on the device, we should skip re-auth |
| // and fill the form. Otherwise the user removing authentication on the |
| // device will prevent them from using payments autofill. In the settings |
| // page, we signal to the user through various means that they need to turn |
| // the device's authentication on in order to use re-auth. |
| if (authentication_method == |
| payments::MandatoryReauthAuthenticationMethod::kUnknown || |
| authentication_method == |
| payments::MandatoryReauthAuthenticationMethod::kUnsupportedMethod) { |
| LogMandatoryReauthCheckoutFlowUsageEvent( |
| payments::MandatoryReauthManager::GetNonInteractivePaymentMethodType( |
| card->record_type()), |
| authentication_method, |
| autofill_metrics::MandatoryReauthAuthenticationFlowEvent::kFlowSkipped); |
| OnCreditCardFetched(*card, /*card_was_fetched_from_cache=*/false); |
| return; |
| } |
| |
| LogMandatoryReauthCheckoutFlowUsageEvent( |
| payments::MandatoryReauthManager::GetNonInteractivePaymentMethodType( |
| card->record_type()), |
| authentication_method, |
| autofill_metrics::MandatoryReauthAuthenticationFlowEvent::kFlowStarted); |
| #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_IOS) |
| autofill_client() |
| .GetPaymentsAutofillClient() |
| ->GetOrCreatePaymentsMandatoryReauthManager() |
| ->AuthenticateWithMessage( |
| l10n_util::GetStringUTF16( |
| IDS_PAYMENTS_AUTOFILL_FILLING_MANDATORY_REAUTH), |
| base::BindOnce(&CreditCardAccessManager:: |
| OnDeviceAuthenticationResponseForFilling, |
| GetWeakPtr(), authentication_method, card)); |
| #elif BUILDFLAG(IS_ANDROID) |
| // TODO(crbug.com/40261690): Convert this to |
| // MandatoryReauthManager::AuthenticateWithMessage() with the correct message |
| // once it is supported. Currently, the message is "Verify it's you". |
| autofill_client() |
| .GetPaymentsAutofillClient() |
| ->GetOrCreatePaymentsMandatoryReauthManager() |
| ->Authenticate(base::BindOnce( |
| &CreditCardAccessManager::OnDeviceAuthenticationResponseForFilling, |
| GetWeakPtr(), authentication_method, card)); |
| #else |
| NOTREACHED(); |
| #endif |
| } |
| |
| void CreditCardAccessManager::OnDeviceAuthenticationResponseForFilling( |
| payments::MandatoryReauthAuthenticationMethod authentication_method, |
| const CreditCard* card, |
| bool successful_auth) { |
| CHECK(card); |
| CreditCard::RecordType record_type = card->record_type(); |
| |
| LogMandatoryReauthCheckoutFlowUsageEvent( |
| payments::MandatoryReauthManager::GetNonInteractivePaymentMethodType( |
| record_type), |
| authentication_method, |
| successful_auth |
| ? autofill_metrics::MandatoryReauthAuthenticationFlowEvent:: |
| kFlowSucceeded |
| : autofill_metrics::MandatoryReauthAuthenticationFlowEvent:: |
| kFlowFailed); |
| if (successful_auth && !card->cvc().empty()) { |
| autofill_metrics::LogCvcFilling( |
| autofill_metrics::CvcFillingFlowType::kMandatoryReauth, record_type); |
| } |
| autofill_metrics::LogServerCardUnmaskResult( |
| successful_auth ? ServerCardUnmaskResult::kAuthenticationUnmasked |
| : ServerCardUnmaskResult::kAuthenticationError, |
| record_type, ServerCardUnmaskFlowType::kDeviceUnlock); |
| |
| if (successful_auth) { |
| OnCreditCardFetched(*card, /*card_was_fetched_from_cache=*/false); |
| } |
| // `on_credit_card_fetched_callback_` makes a copy of `card` and `cvc` before |
| // it asynchronously fills them into the form. Thus we can safely call |
| // `Reset()` here, and we should as from this class' point of view the |
| // authentication flow is complete. |
| Reset(); |
| } |
| |
| void CreditCardAccessManager::OnVcn3dsAuthenticationComplete( |
| payments::PaymentsWindowManager::Vcn3dsAuthenticationResponse response) { |
| if (response.result == |
| payments::PaymentsWindowManager::Vcn3dsAuthenticationResult::kSuccess) { |
| CHECK(response.card.has_value()); |
| OnCreditCardFetched(response.card.value(), |
| /*card_was_fetched_from_cache=*/false); |
| autofill_metrics::LogServerCardUnmaskResult( |
| ServerCardUnmaskResult::kAuthenticationUnmasked, |
| PaymentsRpcCardType::kVirtualCard, |
| ServerCardUnmaskFlowType::kThreeDomainSecure); |
| form_event_logger_->LogCardUnmaskAuthenticationPromptCompleted( |
| unmask_auth_flow_type_); |
| } else { |
| autofill_metrics::LogServerCardUnmaskResult( |
| response.result == payments::PaymentsWindowManager:: |
| Vcn3dsAuthenticationResult::kAuthenticationFailed |
| ? ServerCardUnmaskResult::kVirtualCardRetrievalError |
| : ServerCardUnmaskResult::kFlowCancelled, |
| PaymentsRpcCardType::kVirtualCard, |
| ServerCardUnmaskFlowType::kThreeDomainSecure); |
| } |
| Reset(); |
| } |
| |
| void CreditCardAccessManager::OnCreditCardFetched( |
| const CreditCard& card, |
| bool card_was_fetched_from_cache) { |
| auto* form_data_importer = autofill_client().GetFormDataImporter(); |
| CHECK(form_data_importer); |
| auto& context = form_data_importer->fetched_payments_data_context(); |
| context.fetched_card_instrument_id = card.instrument_id(); |
| context.card_was_fetched_from_cache = card_was_fetched_from_cache; |
| std::move(on_credit_card_fetched_callback_).Run(card); |
| } |
| |
| } // namespace autofill |