blob: 99b5cea9f37c26c99cf30d1945e5fe395b82313b [file] [log] [blame]
// Copyright 2021 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/enterprise/connectors/file_system/signin_dialog_delegate.h"
#include <utility>
#include <vector>
#include "base/strings/stringprintf.h"
#include "chrome/browser/enterprise/connectors/file_system/box_api_call_endpoints.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/grit/generated_resources.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "google_apis/gaia/oauth2_access_token_consumer.h"
#include "google_apis/gaia/oauth2_api_call_flow.h"
#include "net/base/escape.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/views/layout/fill_layout.h"
#include "url/gurl.h"
// TODO(https://crbug.com/1227477): move this class to chrome/browser/ui/views.
namespace {
// The OAuth token consumer name.
const char kOAuthConsumerName[] = "file_system_signin_dialog";
// The OAuth2 configuration of the Box App used for the file system integration
// uses https://google.com/generate_204 as the redirect URI.
// This URI is used because:
// 1/ Chrome is a native app and does not otherwise have an app URI to
// redirect to.
// 2/ An HTTP 204 response is optimal because it is a success.
// 3/ An HTTP 204 response is optimal because it is the smallest
// response that can be generated (it has no body).
// 4/ This URI is used by other native apps for the same purpose.
// 5/ It is controlled by Google.
bool IsOAuth2RedirectURI(const GURL& url) {
return url.host() == "google.com" && url.path() == "/generate_204";
}
gfx::NativeWindow FindMostRelevantContextWindow(
const content::WebContents* web_contents) {
// Can't just use web_contents->GetNativeView(): it results in a dialog that
// disappears upon browser going out of focus and cannot be re-activated or
// closed by user.
auto* browser =
web_contents ? chrome::FindBrowserWithWebContents(web_contents) : nullptr;
// Back up methods are needed to find a window to attach the dialog to,
// because the |web_contents| from |download_item| is stored as a mapping
// inside of it and is not guaranteed to always exist or be valid. Example: if
// the original window got closed when download was still in progress; or if
// we need to resume file upload upon browser restart.
if (!browser) {
LOG(ERROR) << "Can't find window from download item; using active window";
browser = chrome::FindBrowserWithActiveWindow();
}
if (!browser) {
LOG(ERROR) << "Can't find active window; using last active window";
browser = chrome::FindLastActive();
}
DCHECK(browser);
return browser->window()->GetNativeWindow();
}
} // namespace
namespace enterprise_connectors {
////// Authorization Dialog ///////////////////////////////////////////////////
FileSystemSigninDialogDelegate::FileSystemSigninDialogDelegate(
content::BrowserContext* browser_context,
const FileSystemSettings& settings,
AuthorizationCompletedCallback callback)
: settings_(settings),
web_view_(std::make_unique<views::WebView>(browser_context)),
callback_(std::move(callback)) {
SetHasWindowSizeControls(true);
SetTitle(l10n_util::GetStringFUTF16(
IDS_FILE_SYSTEM_CONNECTOR_SIGNIN_DIALOG_TITLE, GetProviderName()));
SetButtons(ui::DIALOG_BUTTON_NONE);
set_use_custom_frame(false);
SetCancelCallback(
base::BindOnce(&FileSystemSigninDialogDelegate::OnCancellation,
weak_factory_.GetWeakPtr()));
AddChildView(web_view_.get());
SetLayoutManager(std::make_unique<views::FillLayout>());
web_modal::WebContentsModalDialogManager::CreateForWebContents(
web_view_->GetWebContents());
web_modal::WebContentsModalDialogManager::FromWebContents(
web_view_->GetWebContents())
->SetDelegate(this);
Observe(web_view_->GetWebContents());
std::string query = base::StringPrintf("client_id=%s&response_type=code",
settings_.client_id.c_str());
std::string extra_params = GetProviderSpecificUrlParameters();
if (!extra_params.empty())
base::StringAppendF(&query, "&%s", extra_params.c_str());
url::Replacements<char> replacements;
replacements.SetQuery(query.c_str(), url::Component(0, query.length()));
GURL url = settings_.authorization_endpoint.ReplaceComponents(replacements);
web_view_->LoadInitialURL(url);
}
FileSystemSigninDialogDelegate::~FileSystemSigninDialogDelegate() {
if (callback_) {
OnCancellation();
}
}
// static
void FileSystemSigninDialogDelegate::ShowDialog(
content::WebContents* web_contents,
const FileSystemSettings& settings,
AuthorizationCompletedCallback callback) {
content::BrowserContext* browser_context = web_contents->GetBrowserContext();
gfx::NativeWindow context = FindMostRelevantContextWindow(web_contents);
std::unique_ptr<FileSystemSigninDialogDelegate> delegate =
std::make_unique<FileSystemSigninDialogDelegate>(
browser_context, settings, std::move(callback));
// We want a dialog whose lifetime is independent from that of |web_contents|,
// therefore using FindMostRelevantContextWindow() as context, instead of
// using web_contents->GetNativeView() as parent. This gives us a new
// top-level window.
auto* widget = views::DialogDelegate::CreateDialogWidget(
std::move(delegate), context, /* parent = */ nullptr);
widget->Show();
}
web_modal::WebContentsModalDialogHost*
FileSystemSigninDialogDelegate::GetWebContentsModalDialogHost() {
return this;
}
gfx::NativeView FileSystemSigninDialogDelegate::GetHostView() const {
return GetWidget()->GetNativeView();
}
gfx::Point FileSystemSigninDialogDelegate::GetDialogPosition(
const gfx::Size& size) {
gfx::Size widget_size = GetWidget()->GetWindowBoundsInScreen().size();
return gfx::Point(std::max(0, (widget_size.width() - size.width()) / 2),
std::max(0, (widget_size.height() - size.height()) / 2));
}
gfx::Size FileSystemSigninDialogDelegate::GetMaximumDialogSize() {
return GetWidget()->GetWindowBoundsInScreen().size();
}
void FileSystemSigninDialogDelegate::AddObserver(
web_modal::ModalDialogHostObserver* observer) {}
void FileSystemSigninDialogDelegate::RemoveObserver(
web_modal::ModalDialogHostObserver* observer) {}
// views::DialogDelegate:
gfx::Size FileSystemSigninDialogDelegate::CalculatePreferredSize() const {
// TODO(https://crbug.com/1159213): need to tweak this.
return gfx::Size(800, 640);
}
ui::ModalType FileSystemSigninDialogDelegate::GetModalType() const {
return ui::MODAL_TYPE_WINDOW;
}
views::View* FileSystemSigninDialogDelegate::GetInitiallyFocusedView() {
return static_cast<views::View*>(web_view_.get());
}
void FileSystemSigninDialogDelegate::OnCancellation() {
std::move(callback_).Run(
GoogleServiceAuthError{GoogleServiceAuthError::State::REQUEST_CANCELED},
std::string(), std::string());
}
void FileSystemSigninDialogDelegate::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
const GURL& url = navigation_handle->GetURL();
if (!IsOAuth2RedirectURI(url)) {
return;
}
std::string auth_code;
// Look for the auth_code. It is found in the code= URL parameter.
if (!net::GetValueForKeyInQuery(url, "code", &auth_code)) {
DLOG(ERROR) << "Failed to extract authorization code from url: " << url;
// TODO(https://crbug.com/1159179): pop dialog about authentication failure?
return;
}
content::StoragePartition* partition =
web_view_->GetBrowserContext()->GetStoragePartitionForUrl(
GURL(kFileSystemBoxEndpointApi));
auto url_loader = partition->GetURLLoaderFactoryForBrowserProcess();
auto callback =
base::BindOnce(&FileSystemSigninDialogDelegate::OnGotOAuthTokens,
weak_factory_.GetWeakPtr());
// No refresh_token, so need to get both tokens with authorization code.
token_fetcher_ = std::make_unique<AccessTokenFetcher>(
url_loader, settings_.service_provider, settings_.token_endpoint,
/*refresh_token=*/std::string(), auth_code, kOAuthConsumerName,
std::move(callback));
token_fetcher_->Start(settings_.client_id, settings_.client_secret,
settings_.scopes);
}
void FileSystemSigninDialogDelegate::OnGotOAuthTokens(
const GoogleServiceAuthError& status,
const std::string& access_token,
const std::string& refresh_token) {
token_fetcher_ = nullptr;
std::move(callback_).Run(status, access_token, refresh_token);
GetWidget()->Close();
}
std::string FileSystemSigninDialogDelegate::GetProviderSpecificUrlParameters() {
// If an email domain is specified, use it as a hint in the box authn URL.
// Make sure the domain has an @ prefix.
if (settings_.service_provider == kBoxProviderName) {
if (!settings_.email_domain.empty()) {
// If the domain does not already start with an @ sign, prepend the
// escaped version of it.
return base::StringPrintf(
"box_login=%s%s", settings_.email_domain[0] == '@' ? "" : "%40",
net::EscapeQueryParamValue(settings_.email_domain, true).c_str());
}
} else {
NOTREACHED() << "Unknown service provider: " << settings_.service_provider;
}
return std::string();
}
std::u16string FileSystemSigninDialogDelegate::GetProviderName() const {
DCHECK_EQ(settings_.service_provider, kBoxProviderName);
return l10n_util::GetStringUTF16(IDS_FILE_SYSTEM_CONNECTOR_BOX);
}
BEGIN_METADATA(FileSystemSigninDialogDelegate, views::DialogDelegateView)
END_METADATA
// TODO(https://crbug.com/1159185): add browser_tests for this delegate.
} // namespace enterprise_connectors