// 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 "content/browser/sms/webotp_service.h" #include #include #include #include #include #include "base/bind.h" #include "base/callback_helpers.h" #include "base/check_op.h" #include "base/command_line.h" #include "base/metrics/histogram_functions.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/sms/sms_metrics.h" #include "content/browser/sms/user_consent_handler.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/sms_fetcher.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "services/metrics/public/cpp/ukm_builders.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/blink/public/common/sms/webotp_constants.h" #include "third_party/blink/public/mojom/sms/webotp_service.mojom-shared.h" using blink::mojom::SmsStatus; using Outcome = blink::WebOTPServiceOutcome; namespace content { namespace { // Only |kMaxUniqueOriginInAncestorChainForWebOTP| unique origins in the chain // is considered valid. In addition, the unique origins must be consecutive. // e.g. the following are valid: // A.com (calls WebOTP API) // A.com -> A.com (calls WebOTP API) // A.com -> A.com -> B.com (calls WebOTP API) // A.com -> B.com -> B.com (calls WebOTP API) // while the following are invalid: // A.com -> B.com -> A.com (calls WebOTP API) // A.com -> B.com -> C.com (calls WebOTP API) bool ValidateAndCollectUniqueOrigins(RenderFrameHost& rfh, OriginList& origin_list) { url::Origin current_origin = rfh.GetLastCommittedOrigin(); origin_list.push_back(current_origin); RenderFrameHost* parent = rfh.GetParent(); while (parent) { url::Origin parent_origin = parent->GetLastCommittedOrigin(); if (!parent_origin.IsSameOriginWith(current_origin)) { origin_list.push_back(parent_origin); current_origin = parent_origin; } if (origin_list.size() > blink::kMaxUniqueOriginInAncestorChainForWebOTP) return false; parent = parent->GetParent(); } return true; } bool IsCrossOriginFrame(RenderFrameHost& rfh) { if (!rfh.GetParent()) return false; url::Origin current_origin = rfh.GetLastCommittedOrigin(); RenderFrameHost* parent = rfh.GetParent(); while (parent) { url::Origin parent_origin = parent->GetLastCommittedOrigin(); if (!parent_origin.IsSameOriginWith(current_origin)) return true; parent = parent->GetParent(); } return false; } Outcome FailureTypeToOutcome(SmsFetchFailureType failure_type) { switch (failure_type) { case SmsFetchFailureType::kPromptTimeout: return Outcome::kTimeout; case SmsFetchFailureType::kPromptCancelled: return Outcome::kUserCancelled; case SmsFetchFailureType::kCrossDeviceFailure: return Outcome::kCrossDeviceFailure; default: NOTREACHED(); return Outcome::kTimeout; } } Outcome SmsStatusToOutcome(SmsStatus status) { switch (status) { case SmsStatus::kSuccess: return Outcome::kSuccess; case SmsStatus::kUnhandledRequest: return Outcome::kUnhandledRequest; case SmsStatus::kAborted: return Outcome::kAborted; case SmsStatus::kCancelled: return Outcome::kCancelled; case SmsStatus::kBackendNotAvailable: // Records when the backend is not available AND the request gets // cancelled. i.e. client specifies GmsBackend.VERIFICATION but it's // unavailable. If client specifies GmsBackend.AUTO and the verification // backend is not available, we fall back to the user consent backend and // the request will be handled accordingly. e.g. if the user declined the // prompt, we record it as |kUserCancelled|. return Outcome::kBackendNotAvailable; case SmsStatus::kTimeout: return Outcome::kTimeout; } } } // namespace WebOTPService::WebOTPService( SmsFetcher* fetcher, const OriginList& origin_list, RenderFrameHost& host, mojo::PendingReceiver receiver) : DocumentService(host, std::move(receiver)), fetcher_(fetcher), origin_list_(origin_list), timeout_timer_(FROM_HERE, blink::kWebOTPRequestTimeout, this, &WebOTPService::OnTimeout) { DCHECK(fetcher_); } WebOTPService::~WebOTPService() { DCHECK(!callback_); } // static bool WebOTPService::Create( SmsFetcher* fetcher, RenderFrameHost* host, mojo::PendingReceiver receiver) { CHECK(host); OriginList origin_list; if (!ValidateAndCollectUniqueOrigins(*host, origin_list)) return false; // WebOTPService owns itself. It will self-destruct when a mojo interface // error occurs, the render frame host is deleted, or the render frame host // navigates to a new document. new WebOTPService(fetcher, origin_list, *host, std::move(receiver)); static_cast(host) ->OnBackForwardCacheDisablingStickyFeatureUsed( blink::scheduler::WebSchedulerTrackedFeature::kWebOTPService); return true; } // static WebOTPService& WebOTPService::CreateForTesting( SmsFetcher* fetcher, const OriginList& origins, RenderFrameHost& frame_host, mojo::PendingReceiver receiver) { return *new WebOTPService(fetcher, origins, frame_host, std::move(receiver)); } void WebOTPService::WillBeDestroyed(DocumentServiceDestructionReason) { // Resolve any pending callback and invoke clean up to unsubscribe this // service from fetcher. // // TODO(https://crbug.com/1317531): Previously, running the callbacks in the // destructor was required to avoid triggering DCHECKs since the // mojo::Receiver was (incorrectly) not yet reset in the destructor. // // The destruction order is fixed so running the reply callbacks should no // longer be necessary; however, there are now unit test-only dependencies on // this behavior. Remove those test dependencies and migrate any remaining // cleanup logic that is still needed to the destructor and delete this // `WillBeDestroyed()` override. CompleteRequest(SmsStatus::kUnhandledRequest); } void WebOTPService::Receive(ReceiveCallback callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(!origin_list_.empty()); // Cancels the last request if there is we have not yet handled it. if (callback_) CompleteRequest(SmsStatus::kCancelled); start_time_ = base::TimeTicks::Now(); callback_ = std::move(callback); timeout_timer_.Reset(); delayed_rejection_reason_.reset(); // |one_time_code_| and prompt are still present from the previous request so // a new subscription is unnecessary. Note that it is only safe for us to use // the in flight otp with the new request since both requests belong to the // same origin. // TODO(majidvp): replace is_active() check with a check on existence of the // handler. auto* consent_handler = GetConsentHandler(); if (consent_handler && consent_handler->is_active()) return; fetcher_->Subscribe(origin_list_, *this, render_frame_host()); } void WebOTPService::OnReceive(const OriginList& origin_list, const std::string& one_time_code, UserConsent consent_requirement) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(!one_time_code_); DCHECK(!start_time_.is_null()); DCHECK(!origin_list.empty()); receive_time_ = base::TimeTicks::Now(); RecordSmsReceiveTime(receive_time_ - start_time_, render_frame_host().GetPageUkmSourceId()); RecordSmsParsingStatus(SmsParsingStatus::kParsed, render_frame_host().GetPageUkmSourceId()); one_time_code_ = one_time_code; // This function cannot get called during prerendering because WebOTPService // is deferred during prerendering by MojoBinderPolicyApplier. This DCHECK // proves we don't have to worry about prerendering when using // WebContents::FromRenderFrameHost() below (see function comments for // WebContents::FromRenderFrameHost() for more details). DCHECK_NE(render_frame_host().GetLifecycleState(), RenderFrameHost::LifecycleState::kPrerendering); WebContents* web_contents = content::WebContents::FromRenderFrameHost(&render_frame_host()); // With UserConsent API, users can see and interact with the permission prompt // when they are on the different page other than the one that calls WebOTP. // This is considered as a bad UX and we should measure how many successful // verifications are exercising the UserConsent backend which is implied by // UserConsent::kObtained. if (consent_requirement == UserConsent::kObtained) { RecordWebContentsVisibilityOnReceive(web_contents->GetVisibility() == Visibility::VISIBLE); } // Create a new consent handler for each OTP request. While we could // potentially cache these across request but they are lightweight enought to // not be worth the complexity associate with caching them. UserConsentHandler* consent_handler = CreateConsentHandler(consent_requirement); consent_handler->RequestUserConsent( one_time_code, base::BindOnce(&WebOTPService::OnUserConsentComplete, weak_ptr_factory_.GetWeakPtr())); } void WebOTPService::OnFailure(FailureType failure_type) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); SmsParser::SmsParsingStatus status = SmsParsingStatus::kParsed; switch (failure_type) { case FailureType::kSmsNotParsed_OTPFormatRegexNotMatch: status = SmsParsingStatus::kOTPFormatRegexNotMatch; break; case FailureType::kSmsNotParsed_HostAndPortNotParsed: status = SmsParsingStatus::kHostAndPortNotParsed; break; case FailureType::kSmsNotParsed_kGURLNotValid: status = SmsParsingStatus::kGURLNotValid; break; case FailureType::kPromptTimeout: case FailureType::kPromptCancelled: case FailureType::kCrossDeviceFailure: // We do not complete the request here and instead rely on |OnTimeout| to // complete the request. This delays the promise resolution for privacy // reasons. e.g. if a promise gets resolved right after a user declines // the prompt, sites would know that the SMS did reach the user and they // could use such information for targeting. By using a timeout in all // cases, it is not possible to distinguish between sms not being received // and received but not shared. // Note that we still unsubscribe it from the fetcher and |Unsubscribe| // will be called again during the normal |CompleteRequest| process but it // should be no-op. delayed_rejection_reason_ = failure_type; fetcher_->Unsubscribe(origin_list_, this); return; case FailureType::kBackendNotAvailable: CompleteRequest(SmsStatus::kBackendNotAvailable); return; case FailureType::kNoFailure: NOTREACHED(); } // Records Sms parsing failures. DCHECK(status != SmsParsingStatus::kParsed); RecordSmsParsingStatus(status, render_frame_host().GetPageUkmSourceId()); } void WebOTPService::Abort() { DCHECK(callback_); CompleteRequest(SmsStatus::kAborted); } void WebOTPService::CompleteRequest(blink::mojom::SmsStatus status) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); absl::optional code = absl::nullopt; if (status == SmsStatus::kSuccess) { DCHECK(one_time_code_); code = one_time_code_; } if (callback_) { RecordMetrics(status); std::move(callback_).Run(status, code); } CleanUp(); } void WebOTPService::CleanUp() { // Skip resetting |one_time_code_|, |sms| and |receive_time_| while prompt is // still open in case it needs to be returned to the next incoming request // upon prompt confirmation. // TODO(majidvp): replace is_active() check with a check on existence of the // handler. auto* consent_handler = GetConsentHandler(); bool consent_in_progress = consent_handler && consent_handler->is_active(); if (!consent_in_progress) { one_time_code_.reset(); receive_time_ = base::TimeTicks(); // Clear the consent handler to avoid reusing it by mistake. consent_handler_.reset(); } start_time_ = base::TimeTicks(); callback_.Reset(); delayed_rejection_reason_.reset(); fetcher_->Unsubscribe(origin_list_, this); } UserConsentHandler* WebOTPService::CreateConsentHandler( UserConsent consent_requirement) { if (consent_handler_for_test_) return consent_handler_for_test_; if (consent_requirement == UserConsent::kNotObtained) { consent_handler_ = std::make_unique( render_frame_host(), origin_list_); } else { consent_handler_ = std::make_unique(); } return consent_handler_.get(); } UserConsentHandler* WebOTPService::GetConsentHandler() { if (consent_handler_for_test_) return consent_handler_for_test_; return consent_handler_.get(); } void WebOTPService::SetConsentHandlerForTesting(UserConsentHandler* handler) { consent_handler_for_test_ = handler; } void WebOTPService::OnTimeout() { CompleteRequest(SmsStatus::kTimeout); } void WebOTPService::RecordMetrics(blink::mojom::SmsStatus status) { // Record ContinueOn timing values only if we are using an asynchronous // consent handler (i.e. showing user prompts). auto* consent_handler = GetConsentHandler(); if (consent_handler && consent_handler->is_async()) { if (status == SmsStatus::kSuccess) { DCHECK(!receive_time_.is_null()); RecordContinueOnSuccessTime(base::TimeTicks::Now() - receive_time_); } else if (delayed_rejection_reason_ && delayed_rejection_reason_.value() == FailureType::kPromptCancelled) { DCHECK(!receive_time_.is_null()); RecordCancelOnSuccessTime(base::TimeTicks::Now() - receive_time_); } } ukm::SourceId source_id = render_frame_host().GetPageUkmSourceId(); ukm::UkmRecorder* recorder = ukm::UkmRecorder::Get(); // For privacy, metrics from inner frames are recorded with the top frame's // origin. Given that WebOTP is supported in cross-origin iframes, it's better // to indicate such information in the |Outcome| metrics to understand the // impact and implications. e.g. does user decline more often if the API is // used in an cross-origin iframe. bool is_cross_origin_frame = IsCrossOriginFrame(render_frame_host()); // For privacy, we do not reject the request immediately when user declines // the permission prompt. Therefore the recording of such outcome is also // delayed. We record it at one of the following scenarios: // 1. at the timeout when the delayed timer fires // 2. before the timeout if the request is aborted // 3. before the timeout if |this| gets destroyed (e.g. website navigates) // 4. before the timeout if the request is cancelled in favor of a new // request by the website. // In 2, 3 and 4, there is a different SmsStatus when trying to record metrics // so we need to do it based on delayed_rejection_reason_. if (delayed_rejection_reason_) { DCHECK_NE(status, SmsStatus::kSuccess); // Records Outcome for requests which we reject with delay. RecordSmsOutcome(FailureTypeToOutcome(delayed_rejection_reason_.value()), source_id, recorder, is_cross_origin_frame); if (delayed_rejection_reason_.value() == FailureType::kPromptCancelled) { RecordSmsUserCancelTime(base::TimeTicks::Now() - start_time_, source_id, recorder); } delayed_rejection_reason_.reset(); return; } // Records Outcome for requests which we resolve / reject immediately. RecordSmsOutcome(SmsStatusToOutcome(status), source_id, recorder, is_cross_origin_frame); if (status == SmsStatus::kSuccess) { RecordSmsSuccessTime(base::TimeTicks::Now() - start_time_, source_id, recorder); } else if (status == SmsStatus::kCancelled) { RecordSmsCancelTime(base::TimeTicks::Now() - start_time_); } } void WebOTPService::OnUserConsentComplete(UserConsentResult result) { switch (result) { case UserConsentResult::kApproved: CompleteRequest(SmsStatus::kSuccess); break; case UserConsentResult::kNoDelegate: case UserConsentResult::kInactiveRenderFrameHost: CompleteRequest(SmsStatus::kCancelled); break; case UserConsentResult::kDenied: OnFailure(FailureType::kPromptCancelled); break; } } } // namespace content