blob: 45fa320d9d9a4b5b91639691313a7699fceebb7f [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/bind.h"
#include "base/callback_helpers.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/metrics/histogram_functions.h"
#include "base/optional.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/navigation_details.h"
#include "content/public/browser/navigation_type.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/blink/public/common/sms/webotp_constants.h"
#include "third_party/blink/public/mojom/sms/webotp_service.mojom-shared.h"
using blink::WebOTPServiceDestroyedReason;
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;
}
} // namespace
WebOTPService::WebOTPService(
SmsFetcher* fetcher,
const OriginList& origin_list,
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::WebOTPService> receiver)
: FrameServiceBase(host, std::move(receiver)),
fetcher_(fetcher),
origin_list_(origin_list),
timeout_timer_(FROM_HERE,
blink::kWebOTPRequestTimeout,
this,
&WebOTPService::OnTimeout) {
DCHECK(fetcher_);
}
WebOTPService::~WebOTPService() {
// Resolve any pending callback and invoke clean up to unsubscribe this
// service from fetcher.
CompleteRequest(SmsStatus::kUnhandledRequest);
DCHECK(!callback_);
}
// static
bool WebOTPService::Create(
SmsFetcher* fetcher,
RenderFrameHost* host,
mojo::PendingReceiver<blink::mojom::WebOTPService> receiver) {
DCHECK(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<RenderFrameHostImpl*>(host)->OnSchedulerTrackedFeatureUsed(
blink::scheduler::WebSchedulerTrackedFeature::kWebOTPService);
return true;
}
void WebOTPService::Receive(ReceiveCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(majidvp): The comment below seems incorrect. This flow is used for
// both prompted and unprompted backends so it is not clear if we should
// always cancel early. Also I don't believe that we are actually silently
// dropping the sms but in fact the logic cancels the request once
// an sms comes in and there is no delegate.
// This flow relies on the delegate to display an infobar for user
// confirmation. Cancelling the call early if no delegate is available is
// easier to debug then silently dropping SMSes later on.
WebContents* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host());
if (!web_contents->GetDelegate()) {
std::move(callback).Run(SmsStatus::kCancelled, base::nullopt);
return;
}
DCHECK(!origin_list_.empty());
// Abort the last request if there is we have not yet handled it.
if (callback_) {
std::move(callback_).Run(SmsStatus::kCancelled, base::nullopt);
fetcher_->Unsubscribe(origin_list_, this);
}
start_time_ = base::TimeTicks::Now();
callback_ = std::move(callback);
timeout_timer_.Reset();
prompt_failure_.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;
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_);
switch (failure_type) {
case FailureType::kPromptTimeout:
case FailureType::kPromptCancelled:
// 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.
prompt_failure_ = failure_type;
fetcher_->Unsubscribe(origin_list_, this);
return;
case FailureType::kBackendNotAvailable:
CompleteRequest(SmsStatus::kBackendNotAvailable);
return;
default: /* do nothing as it is handled below. */
break;
}
// Records Sms parsing failures.
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::kBackendNotAvailable:
case FailureType::kNoFailure:
NOTREACHED();
break;
}
DCHECK(status != SmsParsingStatus::kParsed);
RecordSmsParsingStatus(status, render_frame_host()->GetPageUkmSourceId());
}
void WebOTPService::Abort() {
DCHECK(callback_);
CompleteRequest(SmsStatus::kAborted);
}
void WebOTPService::NavigationEntryCommitted(
const content::LoadCommittedDetails& load_details) {
switch (load_details.type) {
case NavigationType::NAVIGATION_TYPE_NEW_ENTRY:
RecordDestroyedReason(WebOTPServiceDestroyedReason::kNavigateNewPage);
break;
case NavigationType::NAVIGATION_TYPE_EXISTING_ENTRY:
RecordDestroyedReason(
WebOTPServiceDestroyedReason::kNavigateExistingPage);
break;
default:
// Ignore cases we don't care about.
break;
}
}
void WebOTPService::CompleteRequest(blink::mojom::SmsStatus status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::Optional<std::string> code = base::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();
prompt_failure_.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) {
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());
if (status == SmsStatus::kSuccess) {
RecordSmsOutcome(Outcome::kSuccess, source_id, recorder,
is_cross_origin_frame);
RecordSmsSuccessTime(base::TimeTicks::Now() - start_time_, source_id,
recorder);
} else if (status == SmsStatus::kUnhandledRequest) {
RecordSmsOutcome(Outcome::kUnhandledRequest, source_id, recorder,
is_cross_origin_frame);
} else if (status == SmsStatus::kAborted) {
RecordSmsOutcome(Outcome::kAborted, source_id, recorder,
is_cross_origin_frame);
} else if (status == SmsStatus::kCancelled) {
RecordSmsOutcome(Outcome::kCancelled, source_id, recorder,
is_cross_origin_frame);
RecordSmsCancelTime(base::TimeTicks::Now() - start_time_);
} else if (status == SmsStatus::kTimeout) {
if (prompt_failure_ &&
prompt_failure_.value() == FailureType::kPromptCancelled) {
RecordSmsOutcome(Outcome::kUserCancelled, source_id, recorder,
is_cross_origin_frame);
RecordSmsUserCancelTime(base::TimeTicks::Now() - start_time_, source_id,
recorder);
} else {
RecordSmsOutcome(Outcome::kTimeout, source_id, recorder,
is_cross_origin_frame);
}
} else if (status == 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|.
RecordSmsOutcome(Outcome::kBackendNotAvailable, source_id, recorder,
is_cross_origin_frame);
}
// 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 (prompt_failure_ &&
prompt_failure_.value() == FailureType::kPromptCancelled) {
DCHECK(!receive_time_.is_null());
RecordCancelOnSuccessTime(base::TimeTicks::Now() - receive_time_);
}
}
}
void WebOTPService::OnUserConsentComplete(UserConsentResult result) {
switch (result) {
case UserConsentResult::kApproved:
CompleteRequest(SmsStatus::kSuccess);
break;
case UserConsentResult::kNoDelegate:
CompleteRequest(SmsStatus::kCancelled);
break;
case UserConsentResult::kDenied:
OnFailure(FailureType::kPromptCancelled);
break;
}
}
} // namespace content