blob: d017f925861e19b4fa1f11870ae4621bf95678a5 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/extensions/api/identity/web_auth_flow.h"
#include <memory>
#include <utility>
#include "base/base64.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/identity_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/browser_resources.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "crypto/random.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "url/gurl.h"
#include "url/url_constants.h"
using content::RenderViewHost;
using content::WebContents;
using content::WebContentsObserver;
using guest_view::GuestViewBase;
namespace extensions {
namespace {
std::string GetPartitionName(WebAuthFlow::Partition partition) {
switch (partition) {
case WebAuthFlow::LAUNCH_WEB_AUTH_FLOW:
return "launchWebAuthFlow";
case WebAuthFlow::GET_AUTH_TOKEN:
return "getAuthFlow";
}
NOTREACHED() << "Unexpected partition value " << partition;
return std::string();
}
} // namespace
namespace identity_private = api::identity_private;
WebAuthFlow::WebAuthFlow(Delegate* delegate,
Profile* profile,
const GURL& provider_url,
Mode mode,
Partition partition)
: delegate_(delegate),
profile_(profile),
provider_url_(provider_url),
mode_(mode),
partition_(partition),
embedded_window_created_(false) {
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0("identity", "WebAuthFlow", this);
}
WebAuthFlow::~WebAuthFlow() {
DCHECK(!delegate_);
// Stop listening to notifications first since some of the code
// below may generate notifications.
WebContentsObserver::Observe(nullptr);
if (!app_window_key_.empty()) {
AppWindowRegistry::Get(profile_)->RemoveObserver(this);
if (app_window_ && app_window_->web_contents())
app_window_->web_contents()->Close();
}
TRACE_EVENT_NESTABLE_ASYNC_END0("identity", "WebAuthFlow", this);
}
void WebAuthFlow::Start() {
AppWindowRegistry::Get(profile_)->AddObserver(this);
// Attach a random ID string to the window so we can recognize it
// in OnAppWindowAdded.
std::string random_bytes;
crypto::RandBytes(base::WriteInto(&random_bytes, 33), 32);
base::Base64Encode(random_bytes, &app_window_key_);
// identityPrivate.onWebFlowRequest(app_window_key, provider_url_, mode_)
std::unique_ptr<base::ListValue> args(new base::ListValue());
args->AppendString(app_window_key_);
args->AppendString(provider_url_.spec());
if (mode_ == WebAuthFlow::INTERACTIVE)
args->AppendString("interactive");
else
args->AppendString("silent");
args->AppendString(GetPartitionName(partition_));
auto event =
std::make_unique<Event>(events::IDENTITY_PRIVATE_ON_WEB_FLOW_REQUEST,
identity_private::OnWebFlowRequest::kEventName,
std::move(args), profile_);
ExtensionSystem* system = ExtensionSystem::Get(profile_);
extensions::ComponentLoader* component_loader =
system->extension_service()->component_loader();
if (!component_loader->Exists(extension_misc::kIdentityApiUiAppId)) {
component_loader->Add(
IDR_IDENTITY_API_SCOPE_APPROVAL_MANIFEST,
base::FilePath(FILE_PATH_LITERAL("identity_scope_approval_dialog")));
}
EventRouter::Get(profile_)->DispatchEventWithLazyListener(
extension_misc::kIdentityApiUiAppId, std::move(event));
}
void WebAuthFlow::DetachDelegateAndDelete() {
delegate_ = nullptr;
base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, this);
}
content::StoragePartition* WebAuthFlow::GetGuestPartition() {
return content::BrowserContext::GetStoragePartition(
profile_, GetWebViewPartitionConfig(partition_, profile_));
}
const std::string& WebAuthFlow::GetAppWindowKey() const {
return app_window_key_;
}
// static
content::StoragePartitionConfig WebAuthFlow::GetWebViewPartitionConfig(
Partition partition,
content::BrowserContext* browser_context) {
// This has to mirror the logic in WebViewGuest::CreateWebContents for
// creating the correct StoragePartitionConfig.
auto result = content::StoragePartitionConfig::Create(
browser_context, extension_misc::kIdentityApiUiAppId,
GetPartitionName(partition),
/*in_memory=*/true);
result.set_fallback_to_partition_domain_for_blob_urls(
browser_context->IsOffTheRecord()
? content::StoragePartitionConfig::FallbackMode::
kFallbackPartitionInMemory
: content::StoragePartitionConfig::FallbackMode::
kFallbackPartitionOnDisk);
return result;
}
void WebAuthFlow::OnAppWindowAdded(AppWindow* app_window) {
if (app_window->window_key() == app_window_key_ &&
app_window->extension_id() == extension_misc::kIdentityApiUiAppId) {
app_window_ = app_window;
WebContentsObserver::Observe(app_window->web_contents());
}
}
void WebAuthFlow::OnAppWindowRemoved(AppWindow* app_window) {
if (app_window->window_key() == app_window_key_ &&
app_window->extension_id() == extension_misc::kIdentityApiUiAppId) {
app_window_ = nullptr;
WebContentsObserver::Observe(nullptr);
if (delegate_)
delegate_->OnAuthFlowFailure(WebAuthFlow::WINDOW_CLOSED);
}
}
void WebAuthFlow::BeforeUrlLoaded(const GURL& url) {
if (delegate_ && embedded_window_created_)
delegate_->OnAuthFlowURLChange(url);
}
void WebAuthFlow::AfterUrlLoaded() {
if (delegate_ && embedded_window_created_ && mode_ == WebAuthFlow::SILENT)
delegate_->OnAuthFlowFailure(WebAuthFlow::INTERACTION_REQUIRED);
}
void WebAuthFlow::InnerWebContentsCreated(
content::WebContents* inner_web_contents) {
DCHECK(app_window_);
if (!delegate_ || embedded_window_created_)
return;
// Switch from watching the app window to the guest inside it.
embedded_window_created_ = true;
WebContentsObserver::Observe(inner_web_contents);
}
void WebAuthFlow::RenderProcessGone(base::TerminationStatus status) {
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->IsInMainFrame())
BeforeUrlLoaded(navigation_handle->GetURL());
}
void WebAuthFlow::DidRedirectNavigation(
content::NavigationHandle* navigation_handle) {
BeforeUrlLoaded(navigation_handle->GetURL());
}
void WebAuthFlow::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// 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->IsInMainFrame())
return;
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_NESTABLE_ASYNC_INSTANT1(
"identity", "DidFinishNavigationFailure", this, "error_code",
navigation_handle->GetNetErrorCode());
}
} else if (navigation_handle->GetResponseHeaders() &&
navigation_handle->GetResponseHeaders()->response_code() >= 400) {
failed = true;
TRACE_EVENT_NESTABLE_ASYNC_INSTANT1(
"identity", "DidFinishNavigationFailure", this, "response_code",
navigation_handle->GetResponseHeaders()->response_code());
}
if (failed && delegate_)
delegate_->OnAuthFlowFailure(LOAD_FAILED);
}
} // namespace extensions