blob: 2f6c13853401003703dfe94f732fa63e3d504610 [file] [log] [blame]
// 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 <iterator>
#include <memory>
#include <queue>
#include <string>
#include <utility>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.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<blink::mojom::WebOTPService> 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<blink::mojom::WebOTPService> 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 RenderFrameHost is deleted, or the RenderFrameHost
// navigates to a new document.
new WebOTPService(fetcher, origin_list, *host, std::move(receiver));
static_cast<RenderFrameHostImpl*>(host)
->OnBackForwardCacheDisablingStickyFeatureUsed(
blink::scheduler::WebSchedulerTrackedFeature::kWebOTPService);
return true;
}
// static
WebOTPService& WebOTPService::CreateForTesting(
SmsFetcher* fetcher,
const OriginList& origins,
RenderFrameHost& frame_host,
mojo::PendingReceiver<blink::mojom::WebOTPService> 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<std::string> 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<PromptBasedUserConsentHandler>(
render_frame_host(), origin_list_);
} else {
consent_handler_ = std::make_unique<NoopUserConsentHandler>();
}
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