| // Copyright 2023 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/enterprise/profile_management/profile_management_navigation_throttle.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/enterprise/profile_management/profile_management_features.h" |
| #include "chrome/browser/enterprise/profile_management/saml_response_parser.h" |
| #include "chrome/browser/enterprise/signin/profile_token_web_signin_interceptor.h" |
| #include "chrome/browser/enterprise/signin/profile_token_web_signin_interceptor_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profiles_state.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "components/account_id/account_id.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/base/signin_pref_names.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/navigation_throttle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "net/base/url_util.h" |
| #include "services/data_decoder/public/cpp/data_decoder.h" |
| #include "url/gurl.h" |
| |
| namespace profile_management { |
| |
| namespace { |
| |
| constexpr char kGoogleServiceLoginUrl[] = |
| "https://www.google.com/a/%s/ServiceLogin"; |
| |
| // Utility struct used to store SAML attributes related to third-party profile |
| // management. |
| struct SAMLProfileAttributes { |
| std::string name; |
| std::string domain; |
| std::string token; |
| }; |
| |
| constexpr char kNameAttributeKey[] = "name"; |
| constexpr char kDomainAttributeKey[] = "domain"; |
| constexpr char kTokenAttributeKey[] = "token"; |
| |
| base::flat_map<std::string, SAMLProfileAttributes>& GetAttributeMap() { |
| static base::NoDestructor<base::flat_map<std::string, SAMLProfileAttributes>> |
| profile_attributes; |
| |
| if (!profile_attributes->empty()) { |
| return *profile_attributes; |
| } |
| |
| // TODO(crbug.com/40267996): Add actual domains with attribute names. |
| profile_attributes->insert(std::make_pair( |
| "supported.test", |
| SAMLProfileAttributes("placeholderName", "placeholderDomain", |
| "placeholderToken"))); |
| |
| // Extract domains and attributes from the command line switch. |
| const base::CommandLine& command_line = |
| *base::CommandLine::ForCurrentProcess(); |
| if (!command_line.HasSwitch(switches::kProfileManagementAttributes)) { |
| return *profile_attributes; |
| } |
| |
| std::optional<base::Value> switch_value = base::JSONReader::Read( |
| command_line.GetSwitchValueASCII(switches::kProfileManagementAttributes)); |
| if (!switch_value || !switch_value->is_dict()) { |
| VLOG(1) << "[Profile management] Failed to parse attributes JSON."; |
| return *profile_attributes; |
| } |
| |
| for (const auto [domain, attributes_value] : switch_value->GetDict()) { |
| if (!attributes_value.is_dict()) { |
| VLOG(1) << "[Profile management] Attributes for domain \"" << domain |
| << "\" are ignored as they are not in JSON object format."; |
| continue; |
| } |
| |
| const base::Value::Dict& attributes = attributes_value.GetDict(); |
| SAMLProfileAttributes new_attributes; |
| if (auto* name_attribute = attributes.FindString(kNameAttributeKey); |
| name_attribute) { |
| new_attributes.name = *name_attribute; |
| } |
| if (auto* domain_attribute = attributes.FindString(kDomainAttributeKey); |
| domain_attribute) { |
| new_attributes.domain = *domain_attribute; |
| } |
| if (auto* token_attribute = attributes.FindString(kTokenAttributeKey); |
| token_attribute) { |
| new_attributes.token = *token_attribute; |
| } |
| |
| profile_attributes->insert( |
| std::make_pair(domain, std::move(new_attributes))); |
| } |
| |
| VLOG(1) << "[Profile management] Successfully parsed attributes JSON."; |
| return *profile_attributes; |
| } |
| |
| std::optional<std::string> GetDomainFromAttributeValue( |
| const std::string& domain_attribute_value) { |
| // Exclude empty and and dotless domains as they are not supported by the |
| // Google identity service. |
| if (domain_attribute_value.empty() || |
| domain_attribute_value.find(".") == std::string::npos) { |
| return std::nullopt; |
| } |
| |
| // If '@' is found in the domain value, treat it as an email address and |
| // extract the domain from it. |
| if (domain_attribute_value.find("@") != std::string::npos) { |
| std::string email_domain = gaia::ExtractDomainName(domain_attribute_value); |
| return email_domain.empty() ? std::nullopt |
| : std::make_optional(email_domain); |
| } |
| |
| return domain_attribute_value; |
| } |
| |
| // Used to scope posted navigation tasks to the lifetime of `web_contents`. |
| class ProfileManagementWebContentsLifetimeHelper |
| : public content::WebContentsUserData< |
| ProfileManagementWebContentsLifetimeHelper> { |
| public: |
| explicit ProfileManagementWebContentsLifetimeHelper( |
| content::WebContents* web_contents) |
| : content::WebContentsUserData< |
| ProfileManagementWebContentsLifetimeHelper>(*web_contents) {} |
| |
| base::WeakPtr<ProfileManagementWebContentsLifetimeHelper> GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void OpenURL(const content::OpenURLParams& url_params) { |
| GetWebContents().OpenURL(url_params, /*navigation_handle_callback=*/{}); |
| } |
| |
| private: |
| friend class content::WebContentsUserData< |
| ProfileManagementWebContentsLifetimeHelper>; |
| |
| base::WeakPtrFactory<ProfileManagementWebContentsLifetimeHelper> |
| weak_factory_{this}; |
| |
| WEB_CONTENTS_USER_DATA_KEY_DECL(); |
| }; |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(ProfileManagementWebContentsLifetimeHelper); |
| |
| } // namespace |
| |
| // static |
| void ProfileManagementNavigationThrottle::MaybeCreateAndAdd( |
| content::NavigationThrottleRegistry& registry) { |
| if ((!base::FeatureList::IsEnabled(features::kThirdPartyProfileManagement) && |
| !base::FeatureList::IsEnabled( |
| features::kEnableProfileTokenManagement)) || |
| !g_browser_process->local_state() || |
| !profiles::IsProfileCreationAllowed()) { |
| return; |
| } |
| |
| // The throttle is created for all requests since it intercepts specific HTTP |
| // responses. |
| registry.AddThrottle( |
| std::make_unique<ProfileManagementNavigationThrottle>(registry)); |
| } |
| |
| ProfileManagementNavigationThrottle::ProfileManagementNavigationThrottle( |
| content::NavigationThrottleRegistry& registry) |
| : content::NavigationThrottle(registry) {} |
| |
| ProfileManagementNavigationThrottle::~ProfileManagementNavigationThrottle() = |
| default; |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ProfileManagementNavigationThrottle::WillProcessResponse() { |
| if (!base::Contains(GetAttributeMap(), |
| navigation_handle()->GetURL().host())) { |
| return PROCEED; |
| } |
| |
| navigation_handle()->GetResponseBody( |
| base::BindOnce(&ProfileManagementNavigationThrottle::OnResponseBodyReady, |
| weak_ptr_factory_.GetWeakPtr())); |
| return DEFER; |
| } |
| |
| const char* ProfileManagementNavigationThrottle::GetNameForLogging() { |
| return "ProfileManagementNavigationThrottle"; |
| } |
| |
| void ProfileManagementNavigationThrottle::SetURLsForTesting( |
| const std::string& token_url, |
| const std::string& unmanaged_url) { |
| token_url_for_testing_ = token_url; |
| unmanaged_url_for_testing_ = unmanaged_url; |
| } |
| |
| void ProfileManagementNavigationThrottle::ClearAttributeMapForTesting() { |
| GetAttributeMap().clear(); |
| } |
| |
| void ProfileManagementNavigationThrottle::OnResponseBodyReady( |
| const std::string& body) { |
| // TODO(crbug.com/40267996): As a fallback, check more attributes that may |
| // contain the user's email address. |
| const auto profile_attributes = |
| GetAttributeMap().at(navigation_handle()->GetURL().host()); |
| saml_response_parser_ = std::make_unique<SAMLResponseParser>( |
| std::vector<std::string>{profile_attributes.name, |
| profile_attributes.domain, |
| profile_attributes.token}, |
| body, |
| base::BindOnce( |
| &ProfileManagementNavigationThrottle::OnManagementDataReceived, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProfileManagementNavigationThrottle::OnManagementDataReceived( |
| const base::flat_map<std::string, std::string>& attributes) { |
| const std::string navigation_host = navigation_handle()->GetURL().host(); |
| DCHECK(base::Contains(GetAttributeMap(), navigation_host)); |
| const auto profile_attributes = GetAttributeMap().at(navigation_host); |
| |
| if (base::FeatureList::IsEnabled(features::kThirdPartyProfileManagement) && |
| base::Contains(attributes, profile_attributes.domain)) { |
| RegisterWithDomain(attributes.at(profile_attributes.domain)); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| return; |
| } |
| |
| // If the third-party domain-based profile management feature is disabled, or |
| // no domain is found in the response, fall back to token-based management. |
| if (base::FeatureList::IsEnabled(features::kEnableProfileTokenManagement) && |
| base::Contains(attributes, profile_attributes.token)) { |
| RegisterWithToken(base::Contains(attributes, profile_attributes.name) |
| ? attributes.at(profile_attributes.name) |
| : std::string(), |
| attributes.at(profile_attributes.token)); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| return; |
| } |
| |
| if (!unmanaged_url_for_testing_.empty()) { |
| // Used for testing that no profile management flow was entered. |
| PostNavigateTo(GURL(unmanaged_url_for_testing_)); |
| return; |
| } |
| |
| Resume(); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| } |
| |
| void ProfileManagementNavigationThrottle::PostNavigateTo(const GURL& url) { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&ProfileManagementNavigationThrottle::NavigateTo, |
| weak_ptr_factory_.GetWeakPtr(), std::move(url))); |
| } |
| |
| void ProfileManagementNavigationThrottle::NavigateTo(const GURL& url) { |
| content::WebContents* web_contents = navigation_handle()->GetWebContents(); |
| if (!web_contents) { |
| return; |
| } |
| |
| ProfileManagementWebContentsLifetimeHelper::CreateForWebContents( |
| web_contents); |
| ProfileManagementWebContentsLifetimeHelper* helper = |
| ProfileManagementWebContentsLifetimeHelper::FromWebContents(web_contents); |
| |
| // Post a task to navigate to the desired URL since synchronously starting a |
| // navigation from another navigation is an antipattern. This navigation is |
| // cancelled immediately after posting the new navigation. The new navigation |
| // is scoped to the lifetime of `WebContents`. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &ProfileManagementWebContentsLifetimeHelper::OpenURL, |
| helper->GetWeakPtr(), |
| content::OpenURLParams( |
| url, content::Referrer(), WindowOpenDisposition::CURRENT_TAB, |
| ui::PageTransition::PAGE_TRANSITION_CLIENT_REDIRECT, |
| /*is_renderer_initiated=*/false))); |
| |
| // Only call `CancelDeferredNavigation()` outside of testing since it crashes |
| // unit tests. |
| if (token_url_for_testing_.empty() && unmanaged_url_for_testing_.empty()) { |
| CancelDeferredNavigation(content::NavigationThrottle::CANCEL_AND_IGNORE); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| } |
| } |
| |
| void ProfileManagementNavigationThrottle::RegisterWithDomain( |
| const std::string& domain) { |
| std::optional<std::string> management_domain = |
| GetDomainFromAttributeValue(domain); |
| if (management_domain) { |
| auto* prefs = |
| Profile::FromBrowserContext( |
| navigation_handle()->GetWebContents()->GetBrowserContext()) |
| ->GetPrefs(); |
| prefs->SetString(prefs::kSigninInterceptionIDPCookiesUrl, |
| navigation_handle()->GetURL().spec()); |
| PostNavigateTo(GURL(base::StringPrintf(kGoogleServiceLoginUrl, |
| management_domain.value().c_str()))); |
| return; |
| } |
| |
| // Only call `Resume()` outside of testing since it crashes unit tests. |
| if (token_url_for_testing_.empty() && unmanaged_url_for_testing_.empty()) { |
| Resume(); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| } |
| } |
| |
| void ProfileManagementNavigationThrottle::RegisterWithToken( |
| const std::string& name, |
| const std::string& token) { |
| if (!token_url_for_testing_.empty()) { |
| // Used for testing that the token-based management flow was entered. |
| PostNavigateTo(GURL(token_url_for_testing_)); |
| return; |
| } |
| auto* prefs = Profile::FromBrowserContext( |
| navigation_handle()->GetWebContents()->GetBrowserContext()) |
| ->GetPrefs(); |
| prefs->SetString(prefs::kSigninInterceptionIDPCookiesUrl, |
| navigation_handle()->GetURL().spec()); |
| |
| auto* interceptor = ProfileTokenWebSigninInterceptorFactory::GetForProfile( |
| Profile::FromBrowserContext( |
| navigation_handle()->GetWebContents()->GetBrowserContext())); |
| interceptor->MaybeInterceptSigninProfile( |
| navigation_handle()->GetWebContents(), name, token); |
| |
| Resume(); |
| // DO NOT ADD CODE AFTER THIS, as the NavigationThrottle might have been |
| // deleted by the previous call. |
| } |
| |
| } // namespace profile_management |