blob: 5abec5141e77e9db6ebe587ff3852d98f1f7639f [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/api/identity/web_auth_flow.h"
#include <memory>
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/extensions/api/identity/web_auth_flow_info_bar_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#endif
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
using content::WebContents;
using content::WebContentsObserver;
namespace extensions {
WebAuthFlow::WebAuthFlow(
Delegate* delegate,
Profile* profile,
const GURL& provider_url,
Mode mode,
bool user_gesture,
AbortOnLoad abort_on_load_for_non_interactive,
std::optional<base::TimeDelta> timeout_for_non_interactive,
std::optional<gfx::Rect> popup_bounds)
: delegate_(delegate),
profile_(profile),
provider_url_(provider_url),
mode_(mode),
#if BUILDFLAG(ENABLE_EXTENSIONS)
user_gesture_(user_gesture),
#endif
abort_on_load_for_non_interactive_(abort_on_load_for_non_interactive),
timeout_for_non_interactive_(timeout_for_non_interactive),
non_interactive_timeout_timer_(std::make_unique<base::OneShotTimer>()),
popup_bounds_(popup_bounds) {
TRACE_EVENT_BEGIN("identity", "WebAuthFlow",
perfetto::Track::FromPointer(this));
if (timeout_for_non_interactive_) {
DCHECK_GE(*timeout_for_non_interactive_, base::TimeDelta());
DCHECK_LE(*timeout_for_non_interactive_, base::Minutes(1));
}
// profile_ can be null in unit tests.
if (profile_ != nullptr) {
profile_observation_.Observe(profile_);
}
}
WebAuthFlow::~WebAuthFlow() {
DCHECK(!delegate_);
if (web_contents()) {
web_contents()->Close();
}
CloseInfoBar();
// Stop listening to notifications first since some of the code
// below may generate notifications.
WebContentsObserver::Observe(nullptr);
TRACE_EVENT_END("identity", perfetto::Track::FromPointer(this));
}
void WebAuthFlow::SetClockForTesting(
const base::TickClock* tick_clock,
scoped_refptr<base::SequencedTaskRunner> task_runner) {
non_interactive_timeout_timer_ =
std::make_unique<base::OneShotTimer>(tick_clock);
non_interactive_timeout_timer_->SetTaskRunner(task_runner);
}
void WebAuthFlow::Start() {
DCHECK(profile_);
DCHECK(!profile_->IsOffTheRecord());
content::WebContents::CreateParams params(profile_);
web_contents_ = content::WebContents::Create(params);
WebContentsObserver::Observe(web_contents_.get());
content::NavigationController::LoadURLParams load_params(provider_url_);
web_contents_->GetController().LoadURLWithParams(load_params);
MaybeStartTimeout();
}
void WebAuthFlow::DetachDelegateAndDelete() {
delegate_ = nullptr;
// WebAuthFlow must be destroyed asynchronously to avoid reentrancy issues.
//
// WebAuthFlow observes WebContents and notifies its delegate from within
// WebContentsObserver callbacks. The delegate may call
// DetachDelegateAndDelete() in response.
//
// If WebAuthFlow is destroyed synchronously during such a callback, it would
// synchronously destroy its owned WebContents. However, WebContents cannot be
// destroyed while it's in the middle of notifying observers — doing so
// triggers a CHECK().
//
// Therefore, destruction of WebAuthFlow must be deferred to avoid violating
// this constraint. If the Profile is destroyed before the async destruction
// runs, WebAuthFlow will be notified via OnProfileWillBeDestroyed, and the
// WebContents will be explicitly destroyed at that point, ensuring they do
// not outlive the Profile.
base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
this);
}
void WebAuthFlow::DisplayInfoBar() {
DCHECK(web_contents());
info_bar_delegate_ = WebAuthFlowInfoBarDelegate::Create(
web_contents(), info_bar_parameters_.extension_display_name);
}
void WebAuthFlow::CloseInfoBar() {
if (info_bar_delegate_) {
info_bar_delegate_->CloseInfoBar();
}
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
bool WebAuthFlow::DisplayAuthPageInPopupWindow() {
if (Browser::GetCreationStatusForProfile(profile_) !=
Browser::CreationStatus::kOk) {
return false;
}
Browser::CreateParams browser_params(Browser::TYPE_POPUP, profile_,
user_gesture_);
browser_params.omit_from_session_restore = true;
browser_params.should_trigger_session_restore = false;
if (popup_bounds_.has_value()) {
browser_params.initial_bounds = popup_bounds_.value();
}
Browser* browser = Browser::Create(browser_params);
browser->tab_strip_model()->AddWebContents(
std::move(web_contents_), /*index=*/0,
ui::PageTransition::PAGE_TRANSITION_AUTO_TOPLEVEL,
AddTabTypes::ADD_ACTIVE);
browser->window()->Show();
return true;
}
#else
bool WebAuthFlow::DisplayAuthPageInPopupWindow() {
return false;
}
#endif
void WebAuthFlow::BeforeUrlLoaded(const GURL& url) {
if (delegate_) {
delegate_->OnAuthFlowURLChange(url);
}
}
void WebAuthFlow::AfterUrlLoaded() {
CHECK(profile_);
if (profile_->ShutdownStarted()) {
// Don't process further if the profile is being deleted. The pending
// extension functions will be aborted during KeyedService shutdown.
return;
}
initial_url_loaded_ = true;
if (delegate_ && mode_ == WebAuthFlow::SILENT) {
if (abort_on_load_for_non_interactive_ == AbortOnLoad::kYes) {
non_interactive_timeout_timer_->Stop();
delegate_->OnAuthFlowFailure(WebAuthFlow::INTERACTION_REQUIRED);
} else {
// Wait for timeout.
}
return;
}
// If `web_contents_` is nullptr, this means that the interactive tab has
// already been opened once.
if (delegate_ && web_contents_ && mode_ == WebAuthFlow::INTERACTIVE) {
bool is_auth_page_displayed = DisplayAuthPageInPopupWindow();
if (!is_auth_page_displayed) {
delegate_->OnAuthFlowFailure(WebAuthFlow::Failure::CANNOT_CREATE_WINDOW);
return;
}
if (info_bar_parameters_.should_show) {
DisplayInfoBar();
}
}
}
void WebAuthFlow::MaybeStartTimeout() {
if (mode_ != WebAuthFlow::SILENT) {
// Only applies to non-interactive flows.
return;
}
if (abort_on_load_for_non_interactive_ == AbortOnLoad::kYes &&
!timeout_for_non_interactive_) {
// Preserve previous behaviour: no timeout if aborting on load and timeout
// value is not specified.
return;
}
// `base::Unretained(this)` is safe because `this` owns
// `non_interactive_timeout_timer_`.
non_interactive_timeout_timer_->Start(
FROM_HERE,
timeout_for_non_interactive_.value_or(kNonInteractiveMaxTimeout),
base::BindOnce(&WebAuthFlow::OnTimeout, base::Unretained(this)));
}
void WebAuthFlow::OnTimeout() {
if (delegate_) {
delegate_->OnAuthFlowFailure(initial_url_loaded_
? WebAuthFlow::INTERACTION_REQUIRED
: WebAuthFlow::TIMED_OUT);
}
}
void WebAuthFlow::WebContentsDestroyed() {
WebContentsObserver::Observe(nullptr);
if (delegate_) {
delegate_->OnAuthFlowFailure(WebAuthFlow::WINDOW_CLOSED);
}
}
void WebAuthFlow::TitleWasSet(content::NavigationEntry* entry) {
if (delegate_)
delegate_->OnAuthFlowTitleChange(base::UTF16ToUTF8(entry->GetTitle()));
}
void WebAuthFlow::DidStopLoading() {
AfterUrlLoaded();
}
void WebAuthFlow::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (navigation_handle->IsInPrimaryMainFrame()) {
BeforeUrlLoaded(navigation_handle->GetURL());
}
}
void WebAuthFlow::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
if (navigation_handle->IsInPrimaryMainFrame()) {
BeforeUrlLoaded(navigation_handle->GetURL());
}
}
void WebAuthFlow::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
CHECK(profile_);
if (profile_->ShutdownStarted()) {
// Don't process further if the profile is being deleted. The pending
// extension functions will be aborted during KeyedService shutdown.
return;
}
// Websites may create and remove <iframe> during the auth flow. In
// particular, to integrate CAPTCHA tests. Chrome shouldn't abort the auth
// flow if a navigation failed in a sub-frame. https://crbug.com/1049565.
if (!navigation_handle->IsInPrimaryMainFrame())
return;
if (delegate_) {
delegate_->OnNavigationFinished(navigation_handle);
}
bool failed = false;
if (navigation_handle->GetNetErrorCode() != net::OK) {
if (navigation_handle->GetURL().spec() == url::kAboutBlankURL) {
// As part of the OAUth 2.0 protocol with GAIA, at the end of the web
// authorization flow, GAIA redirects to a custom scheme URL of type
// |com.googleusercontent.apps.123:/<extension_id>|, where
// |com.googleusercontent.apps.123| is the reverse DNS notation of the
// client ID of the extension that started the web sign-in flow. (The
// intent of this weird URL scheme was to make sure it couldn't be loaded
// anywhere at all as this makes it much harder to pull off a cross-site
// attack that could leak the returned oauth token to a malicious script
// or site.)
//
// This URL is not an accessible URL from within a Guest WebView, so
// during its load of this URL, Chrome changes it to |about:blank| and
// then the Identity Scope Approval Dialog extension fails to load it.
// Failing to load |about:blank| must not be treated as a failure of
// the web auth flow.
DCHECK_EQ(net::ERR_UNKNOWN_URL_SCHEME,
navigation_handle->GetNetErrorCode());
} else if (navigation_handle->GetResponseHeaders() &&
navigation_handle->GetResponseHeaders()->response_code() ==
net::HTTP_NO_CONTENT) {
// Navigation to no content URLs is aborted but shouldn't be treated as a
// failure.
// In particular, Gaia navigates to a no content page to pass Mirror
// response headers.
} else {
failed = true;
TRACE_EVENT_INSTANT("identity", "DidFinishNavigationFailure",
perfetto::Track::FromPointer(this), "error_code",
navigation_handle->GetNetErrorCode());
}
} else if (navigation_handle->GetResponseHeaders() &&
navigation_handle->GetResponseHeaders()->response_code() >= 400) {
failed = true;
TRACE_EVENT_INSTANT(
"identity", "DidFinishNavigationFailure",
perfetto::Track::FromPointer(this), "response_code",
navigation_handle->GetResponseHeaders()->response_code());
}
if (failed && delegate_) {
delegate_->OnAuthFlowFailure(LOAD_FAILED);
}
}
void WebAuthFlow::OnProfileWillBeDestroyed(Profile* profile) {
CHECK_EQ(profile, profile_);
profile_observation_.Reset();
// Null out the delegate early so that we do not call into it while
// WebContents are being destroyed. It would be cleaner to send a "profile
// destroyed" notification to the delegate, but all the current delegates
// already observe Profile destruction, so we can just be silent here.
delegate_ = nullptr;
// Destroy the WebContents so that they don't outlive the profile.
if (web_contents()) {
web_contents()->Close();
}
WebContentsObserver::Observe(nullptr);
web_contents_.reset();
profile_ = nullptr;
}
void WebAuthFlow::SetShouldShowInfoBar(
const std::string& extension_display_name) {
info_bar_parameters_.should_show = true;
info_bar_parameters_.extension_display_name = extension_display_name;
}
base::WeakPtr<WebAuthFlowInfoBarDelegate>
WebAuthFlow::GetInfoBarDelegateForTesting() {
return info_bar_delegate_;
}
} // namespace extensions