blob: 9481d62f32d603f28504ce2b652c8650107ac013 [file] [log] [blame]
// Copyright 2014 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 <cstring>
#include <memory>
#include <string>
#include <utility>
#include "ash/public/cpp/ash_features.h"
#include "base/base64.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/test/bind_test_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/chromeos/login/existing_user_controller.h"
#include "chrome/browser/chromeos/login/screens/gaia_view.h"
#include "chrome/browser/chromeos/login/startup_utils.h"
#include "chrome/browser/chromeos/login/test/enrollment_ui_mixin.h"
#include "chrome/browser/chromeos/login/test/fake_gaia_mixin.h"
#include "chrome/browser/chromeos/login/test/https_forwarder.h"
#include "chrome/browser/chromeos/login/test/js_checker.h"
#include "chrome/browser/chromeos/login/test/local_policy_test_server_mixin.h"
#include "chrome/browser/chromeos/login/test/login_screen_tester.h"
#include "chrome/browser/chromeos/login/test/oobe_base_test.h"
#include "chrome/browser/chromeos/login/test/oobe_screen_waiter.h"
#include "chrome/browser/chromeos/login/ui/login_display_host.h"
#include "chrome/browser/chromeos/login/users/chrome_user_manager.h"
#include "chrome/browser/chromeos/login/wizard_controller.h"
#include "chrome/browser/chromeos/policy/affiliation_test_helper.h"
#include "chrome/browser/chromeos/policy/device_policy_builder.h"
#include "chrome/browser/chromeos/policy/device_policy_cros_browser_test.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/signin/signin_utils.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/constants/chromeos_switches.h"
#include "chromeos/cryptohome/system_salt_getter.h"
#include "chromeos/dbus/cryptohome/cryptohome_client.h"
#include "chromeos/dbus/cryptohome/fake_cryptohome_client.h"
#include "chromeos/dbus/cryptohome/key.pb.h"
#include "chromeos/dbus/cryptohome/rpc.pb.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/session_manager/fake_session_manager_client.h"
#include "chromeos/dbus/session_manager/session_manager_client.h"
#include "chromeos/dbus/shill/shill_manager_client.h"
#include "chromeos/login/auth/key.h"
#include "chromeos/settings/cros_settings_names.h"
#include "components/account_id/account_id.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/guest_view/browser/test_guest_view_manager.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_switches.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "components/policy/proto/chrome_device_policy.pb.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "extensions/common/features/feature_channel.h"
#include "google_apis/gaia/fake_gaia.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_switches.h"
#include "google_apis/gaia/gaia_urls.h"
#include "net/base/url_util.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_store.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"
namespace em = enterprise_management;
using net::test_server::BasicHttpResponse;
using net::test_server::HttpRequest;
using net::test_server::HttpResponse;
using testing::_;
using testing::Return;
namespace chromeos {
namespace {
constexpr char kGAIASIDCookieName[] = "SID";
constexpr char kGAIALSIDCookieName[] = "LSID";
constexpr char kTestAuthSIDCookie1[] = "fake-auth-SID-cookie-1";
constexpr char kTestAuthSIDCookie2[] = "fake-auth-SID-cookie-2";
constexpr char kTestAuthLSIDCookie1[] = "fake-auth-LSID-cookie-1";
constexpr char kTestAuthLSIDCookie2[] = "fake-auth-LSID-cookie-2";
// Note: SAML account cannot be @gmail or @example.com account. The former by
// design, the latter because @example.com is used in another tests as regular
// user. So we use @corp.example.com and @example.test, so that we can handle
// it specially in embedded_setup_chromeos.html .
constexpr char kFirstSAMLUserEmail[] = "bob@corp.example.com";
constexpr char kSecondSAMLUserEmail[] = "alice@corp.example.com";
constexpr char kHTTPSAMLUserEmail[] = "carol@corp.example.com";
constexpr char kNonSAMLUserEmail[] = "dan@corp.example.com";
constexpr char kDifferentDomainSAMLUserEmail[] = "eve@example.test";
constexpr char kFirstSAMLUserGaiaId[] = "bob-gaia";
constexpr char kSecondSAMLUserGaiaId[] = "alice-gaia";
constexpr char kHTTPSAMLUserGaiaId[] = "carol-gaia";
constexpr char kNonSAMLUserGaiaId[] = "dan-gaia";
constexpr char kDifferentDomainSAMLUserGaiaId[] = "eve-gaia";
constexpr char kIdPHost[] = "login.corp.example.com";
constexpr char kAdditionalIdPHost[] = "login2.corp.example.com";
constexpr char kSAMLIdPCookieName[] = "saml";
constexpr char kSAMLIdPCookieValue1[] = "value-1";
constexpr char kSAMLIdPCookieValue2[] = "value-2";
constexpr char kRelayState[] = "RelayState";
constexpr char kTestUserinfoToken[] = "fake-userinfo-token";
constexpr char kTestRefreshToken[] = "fake-refresh-token";
constexpr char kAffiliationID[] = "some-affiliation-id";
// FakeSamlIdp serves IdP auth form and the form submission. The form is
// served with the template's RelayState placeholder expanded to the real
// RelayState parameter from request. The form submission redirects back to
// FakeGaia with the same RelayState.
class FakeSamlIdp {
public:
FakeSamlIdp();
~FakeSamlIdp();
void SetUp(const std::string& base_path, const GURL& gaia_url);
void SetLoginHTMLTemplate(const std::string& template_file);
void SetLoginAuthHTMLTemplate(const std::string& template_file);
void SetRefreshURL(const GURL& refresh_url);
void SetCookieValue(const std::string& cookie_value);
void SetSamlResponseFile(const std::string& xml_file);
std::unique_ptr<HttpResponse> HandleRequest(const HttpRequest& request);
private:
std::unique_ptr<HttpResponse> BuildHTMLResponse(
const std::string& html_template,
const std::string& relay_state,
const std::string& next_path);
base::FilePath html_template_dir_;
base::FilePath saml_response_dir_;
std::string login_path_;
std::string login_auth_path_;
std::string login_html_template_;
std::string login_auth_html_template_;
GURL gaia_assertion_url_;
GURL refresh_url_;
std::string cookie_value_;
std::string saml_response_{"fake_response"};
DISALLOW_COPY_AND_ASSIGN(FakeSamlIdp);
};
FakeSamlIdp::FakeSamlIdp() {}
FakeSamlIdp::~FakeSamlIdp() {}
void FakeSamlIdp::SetUp(const std::string& base_path, const GURL& gaia_url) {
base::FilePath test_data_dir;
ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir));
// NOTE: Ideally testdata would all be in chromeos/login, to match the test.
html_template_dir_ = test_data_dir.Append("login");
saml_response_dir_ = test_data_dir.Append("chromeos").Append("login");
login_path_ = base_path;
login_auth_path_ = base_path + "Auth";
gaia_assertion_url_ = gaia_url.Resolve("/SSO");
}
void FakeSamlIdp::SetLoginHTMLTemplate(const std::string& template_file) {
base::ScopedAllowBlockingForTesting allow_io;
EXPECT_TRUE(base::ReadFileToString(html_template_dir_.Append(template_file),
&login_html_template_));
}
void FakeSamlIdp::SetLoginAuthHTMLTemplate(const std::string& template_file) {
base::ScopedAllowBlockingForTesting allow_io;
EXPECT_TRUE(base::ReadFileToString(html_template_dir_.Append(template_file),
&login_auth_html_template_));
}
void FakeSamlIdp::SetSamlResponseFile(const std::string& xml_file) {
base::ScopedAllowBlockingForTesting allow_io;
EXPECT_TRUE(base::ReadFileToString(saml_response_dir_.Append(xml_file),
&saml_response_));
base::Base64Encode(saml_response_, &saml_response_);
}
void FakeSamlIdp::SetRefreshURL(const GURL& refresh_url) {
refresh_url_ = refresh_url;
}
void FakeSamlIdp::SetCookieValue(const std::string& cookie_value) {
cookie_value_ = cookie_value;
}
std::unique_ptr<HttpResponse> FakeSamlIdp::HandleRequest(
const HttpRequest& request) {
// The scheme and host of the URL is actually not important but required to
// get a valid GURL in order to parse |request.relative_url|.
GURL request_url = GURL("http://localhost").Resolve(request.relative_url);
std::string request_path = request_url.path();
if (request_path == login_path_) {
std::string relay_state;
net::GetValueForKeyInQuery(request_url, kRelayState, &relay_state);
return BuildHTMLResponse(login_html_template_, relay_state,
login_auth_path_);
}
if (request_path != login_auth_path_) {
// Request not understood.
return std::unique_ptr<HttpResponse>();
}
std::string relay_state;
FakeGaia::GetQueryParameter(request.content, kRelayState, &relay_state);
GURL redirect_url = gaia_assertion_url_;
if (!login_auth_html_template_.empty()) {
return BuildHTMLResponse(login_auth_html_template_, relay_state,
redirect_url.spec());
}
redirect_url =
net::AppendQueryParameter(redirect_url, "SAMLResponse", saml_response_);
redirect_url =
net::AppendQueryParameter(redirect_url, kRelayState, relay_state);
std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse());
http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
http_response->AddCustomHeader("Location", redirect_url.spec());
http_response->AddCustomHeader(
"Set-cookie", base::StringPrintf("saml=%s", cookie_value_.c_str()));
return std::move(http_response);
}
std::unique_ptr<HttpResponse> FakeSamlIdp::BuildHTMLResponse(
const std::string& html_template,
const std::string& relay_state,
const std::string& next_path) {
std::string response_html = html_template;
base::ReplaceSubstringsAfterOffset(&response_html, 0, "$RelayState",
relay_state);
base::ReplaceSubstringsAfterOffset(&response_html, 0, "$Post", next_path);
base::ReplaceSubstringsAfterOffset(&response_html, 0, "$Refresh",
refresh_url_.spec());
std::unique_ptr<BasicHttpResponse> http_response(new BasicHttpResponse());
http_response->set_code(net::HTTP_OK);
http_response->set_content(response_html);
http_response->set_content_type("text/html");
return std::move(http_response);
}
// A FakeCryptohomeClient that stores the salted and hashed secret passed to
// MountEx().
class SecretInterceptingFakeCryptohomeClient : public FakeCryptohomeClient {
public:
SecretInterceptingFakeCryptohomeClient();
void MountEx(const cryptohome::AccountIdentifier& id,
const cryptohome::AuthorizationRequest& auth,
const cryptohome::MountRequest& request,
DBusMethodCallback<cryptohome::BaseReply> callback) override;
const std::string& salted_hashed_secret() { return salted_hashed_secret_; }
private:
std::string salted_hashed_secret_;
DISALLOW_COPY_AND_ASSIGN(SecretInterceptingFakeCryptohomeClient);
};
SecretInterceptingFakeCryptohomeClient::
SecretInterceptingFakeCryptohomeClient() {}
void SecretInterceptingFakeCryptohomeClient::MountEx(
const cryptohome::AccountIdentifier& id,
const cryptohome::AuthorizationRequest& auth,
const cryptohome::MountRequest& request,
DBusMethodCallback<cryptohome::BaseReply> callback) {
salted_hashed_secret_ = auth.key().secret();
FakeCryptohomeClient::MountEx(id, auth, request, std::move(callback));
}
} // namespace
class SamlTest : public OobeBaseTest {
public:
SamlTest() { fake_gaia_.set_initialize_fake_merge_session(false); }
~SamlTest() override {}
void SetUpCommandLine(base::CommandLine* command_line) override {
OobeBaseTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(switches::kOobeSkipPostLogin);
command_line->AppendSwitch(
chromeos::switches::kAllowFailedPolicyFetchForTest);
ASSERT_TRUE(saml_https_forwarder_.Initialize(
kIdPHost, embedded_test_server()->base_url()));
const GURL gaia_url =
fake_gaia_.gaia_https_forwarder()->GetURLForSSLHost("");
const GURL saml_idp_url = saml_https_forwarder_.GetURLForSSLHost("SAML");
fake_saml_idp_.SetUp(saml_idp_url.path(), gaia_url);
fake_gaia_.fake_gaia()->RegisterSamlUser(kFirstSAMLUserEmail, saml_idp_url);
fake_gaia_.fake_gaia()->RegisterSamlUser(kSecondSAMLUserEmail,
saml_idp_url);
fake_gaia_.fake_gaia()->RegisterSamlUser(
kHTTPSAMLUserEmail,
embedded_test_server()->base_url().Resolve("/SAML"));
fake_gaia_.fake_gaia()->RegisterSamlUser(kDifferentDomainSAMLUserEmail,
saml_idp_url);
fake_gaia_.fake_gaia()->RegisterSamlDomainRedirectUrl("example.com",
saml_idp_url);
}
void SetUpInProcessBrowserTestFixture() override {
// Creates a fake CryptohomeClient. Will be destroyed in browser shutdown.
cryptohome_client_ = new SecretInterceptingFakeCryptohomeClient;
OobeBaseTest::SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
OobeBaseTest::TearDownInProcessBrowserTestFixture();
}
void SetUpOnMainThread() override {
fake_gaia_.fake_gaia()->SetFakeMergeSessionParams(
kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
embedded_test_server()->RegisterRequestHandler(base::Bind(
&FakeSamlIdp::HandleRequest, base::Unretained(&fake_saml_idp_)));
OobeBaseTest::SetUpOnMainThread();
}
void SetupAuthFlowChangeListener() {
content::ExecuteScriptAsync(
GetLoginUI()->GetWebContents(),
"$('gaia-signin').gaiaAuthHost_.addEventListener('authFlowChange',"
" function f() {"
" $('gaia-signin').gaiaAuthHost_.removeEventListener("
" 'authFlowChange', f);"
" window.domAutomationController.send("
" $('gaia-signin').isSAML() ? 'SamlLoaded' : 'GaiaLoaded');"
" });");
}
virtual void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) {
WaitForSigninScreen();
content::DOMMessageQueue message_queue; // Start observe before SAML.
SetupAuthFlowChangeListener();
LoginDisplayHost::default_host()
->GetOobeUI()
->GetGaiaScreenView()
->ShowSigninScreenForTest(gaia_email, "", "[]");
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"SamlLoaded\"");
}
void SendConfirmPassword(const std::string& password_to_confirm) {
std::string js =
"$('saml-confirm-password').$.passwordInput.value='$Password';"
"$('saml-confirm-password').$.inputForm.submit();";
base::ReplaceSubstringsAfterOffset(&js, 0, "$Password",
password_to_confirm);
ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(), js));
}
void SetManualPasswords(const std::string& password,
const std::string& confirm_password) {
std::string js =
"$('saml-confirm-password').$.passwordInput.value='$Password';"
"$('saml-confirm-password').$$('#confirmPasswordInput').value="
" '$ConfirmPassword';"
"$('saml-confirm-password').$.inputForm.submit();";
base::ReplaceSubstringsAfterOffset(&js, 0, "$Password", password);
base::ReplaceSubstringsAfterOffset(&js, 0, "$ConfirmPassword",
confirm_password);
ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(), js));
}
std::string WaitForAndGetFatalErrorMessage() {
OobeScreenWaiter(OobeScreen::SCREEN_FATAL_ERROR).Wait();
std::string message_element = "$('fatal-error-card')";
std::string error_message;
if (!content::ExecuteScriptAndExtractString(
GetLoginUI()->GetWebContents(),
"window.domAutomationController.send(" + message_element +
".textContent);",
&error_message)) {
ADD_FAILURE();
}
return error_message;
}
FakeSamlIdp* fake_saml_idp() { return &fake_saml_idp_; }
protected:
HTTPSForwarder saml_https_forwarder_;
SecretInterceptingFakeCryptohomeClient* cryptohome_client_;
FakeGaiaMixin fake_gaia_{&mixin_host_, embedded_test_server()};
private:
FakeSamlIdp fake_saml_idp_;
DISALLOW_COPY_AND_ASSIGN(SamlTest);
};
// Tests that signin frame should have 'saml' class and 'cancel' button is
// visible when SAML IdP page is loaded. And 'cancel' button goes back to
// gaia on clicking.
IN_PROC_BROWSER_TEST_F(SamlTest, SamlUI) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
// Saml flow UI expectations.
test::OobeJS().ExpectHasClass("full-width", {"gaia-signin"});
test::OobeJS().ExpectVisible("saml-notice-container");
std::string js = "$('saml-notice-message').textContent.indexOf('$Host') > -1";
base::ReplaceSubstringsAfterOffset(&js, 0, "$Host", kIdPHost);
test::OobeJS().ExpectTrue(js);
content::DOMMessageQueue message_queue; // Observe before 'cancel'.
SetupAuthFlowChangeListener();
// Click on 'cancel'.
content::ExecuteScriptAsync(GetLoginUI()->GetWebContents(),
"$('gaia-navigation').$.closeButton.click();");
// Auth flow should change back to Gaia.
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"GaiaLoaded\"");
// Saml flow is gone.
test::OobeJS().ExpectHasNoClass("full-width", {"gaia-signin"});
}
// Tests the sign-in flow when the credentials passing API is used.
IN_PROC_BROWSER_TEST_F(SamlTest, CredentialPassingAPI) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_api_login.html");
fake_saml_idp()->SetLoginAuthHTMLTemplate("saml_api_login_auth.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("not_the_password", {"Dummy"});
SigninFrameJS().TypeIntoPath("actual_password", {"Password"});
SigninFrameJS().TapOn("Submit");
// Login should finish login and a session should start.
session_start_waiter.Wait();
// Regression test for http://crbug.com/490737: Verify that the user's actual
// password was used, not the contents of the first type=password input field
// found on the page.
Key key("actual_password");
key.Transform(Key::KEY_TYPE_SALTED_SHA256_TOP_HALF,
SystemSaltGetter::ConvertRawSaltToHexString(
FakeCryptohomeClient::GetStubSystemSalt()));
EXPECT_EQ(key.GetSecret(), cryptohome_client_->salted_hashed_secret());
}
// Tests the single password scraped flow.
IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedSingle) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
content::DOMMessageQueue message_queue;
// Make sure that the password is scraped correctly.
ASSERT_TRUE(content::ExecuteScript(
GetLoginUI()->GetWebContents(),
"$('gaia-signin').gaiaAuthHost_.addEventListener('authCompleted',"
" function(e) {"
" var password = e.detail.password;"
" window.domAutomationController.send(password);"
" });"));
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
// Scraping a single password should finish the login and start the session.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"fake_password\"");
session_start_waiter.Wait();
}
// Tests password scraping from a dynamically created password field.
IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedDynamic) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
SigninFrameJS().Evaluate(
"(function() {"
" var newPassInput = document.createElement('input');"
" newPassInput.id = 'DynamicallyCreatedPassword';"
" newPassInput.type = 'password';"
" newPassInput.name = 'Password';"
" document.forms[0].appendChild(newPassInput);"
"})();");
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"DynamicallyCreatedPassword"});
// Scraping a single password should finish the login and start the session.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
session_start_waiter.Wait();
}
// Tests the multiple password scraped flow.
IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedMultiple) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login_two_passwords.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
SigninFrameJS().TypeIntoPath("password1", {"Password1"});
SigninFrameJS().TapOn("Submit");
// Lands on confirm password screen.
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("!$('saml-confirm-password').manualInput");
// Entering an unknown password should go back to the confirm password screen.
SendConfirmPassword("wrong_password");
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("!$('saml-confirm-password').manualInput");
// Either scraped password should be able to sign-in.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SendConfirmPassword("password1");
session_start_waiter.Wait();
}
// Tests the no password scraped flow.
IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedNone) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login_no_passwords.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TapOn("Submit");
// Lands on confirm password screen with manual input state.
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("$('saml-confirm-password').manualInput");
// Entering passwords that don't match will make us land again in the same
// page.
SetManualPasswords("Test1", "Test2");
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("$('saml-confirm-password').manualInput");
// Two matching passwords should let the user to sign in.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SetManualPasswords("Test1", "Test1");
session_start_waiter.Wait();
}
// Types |bob@corp.example.com| into the GAIA login form but then authenticates
// as |alice@corp.example.com| via SAML. Verifies that the logged-in user is
// correctly identified as Alice.
IN_PROC_BROWSER_TEST_F(SamlTest, UseAutenticatedUserEmailAddress) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
// Type |bob@corp.example.com| into the GAIA login form.
StartSamlAndWaitForIdpPageLoad(kSecondSAMLUserEmail);
// Authenticate as alice@corp.example.com via SAML (the |Email| provided here
// is irrelevant - the authenticated user's e-mail address that FakeGAIA
// reports was set via |SetFakeMergeSessionParams|).
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
session_start_waiter.Wait();
const user_manager::User* user =
user_manager::UserManager::Get()->GetActiveUser();
ASSERT_TRUE(user);
EXPECT_EQ(kFirstSAMLUserEmail, user->GetAccountId().GetUserEmail());
}
// Verifies that if the authenticated user's e-mail address cannot be retrieved,
// an error message is shown.
IN_PROC_BROWSER_TEST_F(SamlTest, FailToRetrieveAutenticatedUserEmailAddress) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
fake_gaia_.fake_gaia()->SetFakeMergeSessionParams("", kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
SigninFrameJS().TapOn("Submit");
EXPECT_EQ(l10n_util::GetStringUTF8(IDS_LOGIN_FATAL_ERROR_NO_ACCOUNT_DETAILS),
WaitForAndGetFatalErrorMessage());
}
// Tests the password confirm flow when more than one password is scraped: show
// error on the first failure and fatal error on the second failure.
IN_PROC_BROWSER_TEST_F(SamlTest, PasswordConfirmFlow) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login_two_passwords.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
SigninFrameJS().TypeIntoPath("password1", {"Password1"});
SigninFrameJS().TapOn("Submit");
// Lands on confirm password screen with no error message.
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("!$('saml-confirm-password').manualInput");
test::OobeJS().ExpectTrue(
"!$('saml-confirm-password').$.passwordInput.isInvalid");
// Enter an unknown password for the first time should go back to confirm
// password screen with error message.
SendConfirmPassword("wrong_password");
OobeScreenWaiter(OobeScreen::SCREEN_CONFIRM_PASSWORD).Wait();
test::OobeJS().ExpectTrue("!$('saml-confirm-password').manualInput");
test::OobeJS().ExpectTrue(
"$('saml-confirm-password').$.passwordInput.isInvalid");
// Enter an unknown password 2nd time should go back fatal error message.
SendConfirmPassword("wrong_password");
EXPECT_EQ(
l10n_util::GetStringUTF8(IDS_LOGIN_FATAL_ERROR_PASSWORD_VERIFICATION),
WaitForAndGetFatalErrorMessage());
}
// Verifies that when the login flow redirects from one host to another, the
// notice shown to the user is updated. This guards against regressions of
// http://crbug.com/447818.
IN_PROC_BROWSER_TEST_F(SamlTest, NoticeUpdatedOnRedirect) {
// Start another https server at |kAdditionalIdPHost|.
HTTPSForwarder saml_https_forwarder_2;
ASSERT_TRUE(saml_https_forwarder_2.Initialize(
kAdditionalIdPHost, embedded_test_server()->base_url()));
// Make the login flow redirect to |kAdditionalIdPHost|.
fake_saml_idp()->SetLoginHTMLTemplate("saml_login_instant_meta_refresh.html");
fake_saml_idp()->SetRefreshURL(
saml_https_forwarder_2.GetURLForSSLHost("simple.html"));
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
// Wait until the notice shown to the user is updated to contain
// |kAdditionalIdPHost|.
std::string js =
"var sendIfHostFound = function() {"
" var found ="
" $('saml-notice-message').textContent.indexOf('$Host') > -1;"
" if (found)"
" window.domAutomationController.send(true);"
" return found;"
"};"
"var processEventsAndSendIfHostFound = function() {"
" window.setTimeout(function() {"
" if (sendIfHostFound()) {"
" $('gaia-signin').gaiaAuthHost_.removeEventListener("
" 'authDomainChange',"
" processEventsAndSendIfHostFound);"
" }"
" }, 0);"
"};"
"if (!sendIfHostFound()) {"
" $('gaia-signin').gaiaAuthHost_.addEventListener("
" 'authDomainChange',"
" processEventsAndSendIfHostFound);"
"}";
base::ReplaceSubstringsAfterOffset(&js, 0, "$Host", kAdditionalIdPHost);
bool dummy;
EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
GetLoginUI()->GetWebContents(), js, &dummy));
// Verify that the notice is visible.
test::OobeJS().ExpectVisible("saml-notice-container");
}
// Verifies that when GAIA attempts to redirect to a SAML IdP served over http,
// not https, the redirect is blocked and an error message is shown.
IN_PROC_BROWSER_TEST_F(SamlTest, HTTPRedirectDisallowed) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
WaitForSigninScreen();
LoginDisplayHost::default_host()
->GetOobeUI()
->GetGaiaScreenView()
->ShowSigninScreenForTest(kHTTPSAMLUserEmail, "", "[]");
const GURL url = embedded_test_server()->base_url().Resolve("/SAML");
EXPECT_EQ(l10n_util::GetStringFUTF8(IDS_LOGIN_FATAL_ERROR_TEXT_INSECURE_URL,
base::UTF8ToUTF16(url.spec())),
WaitForAndGetFatalErrorMessage());
}
// Verifies that when GAIA attempts to redirect to a page served over http, not
// https, via an HTML meta refresh, the redirect is blocked and an error message
// is shown. This guards against regressions of http://crbug.com/359515.
IN_PROC_BROWSER_TEST_F(SamlTest, MetaRefreshToHTTPDisallowed) {
const GURL url = embedded_test_server()->base_url().Resolve("/SSO");
fake_saml_idp()->SetLoginHTMLTemplate("saml_login_instant_meta_refresh.html");
fake_saml_idp()->SetRefreshURL(url);
WaitForSigninScreen();
LoginDisplayHost::default_host()
->GetOobeUI()
->GetGaiaScreenView()
->ShowSigninScreenForTest(kFirstSAMLUserEmail, "", "[]");
EXPECT_EQ(l10n_util::GetStringFUTF8(IDS_LOGIN_FATAL_ERROR_TEXT_INSECURE_URL,
base::UTF8ToUTF16(url.spec())),
WaitForAndGetFatalErrorMessage());
}
// Verifies that information about the user's password (specifically, whether
// is has expired) can be extracted from the SAMLResponse from the IdP.
IN_PROC_BROWSER_TEST_F(SamlTest, ExtractPasswordAttributes) {
// TODO(https://crbug.com/930109): Replace this with an end-to-end test that
// tests the actual functionality, once this is implemented.
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
fake_saml_idp()->SetSamlResponseFile("saml_with_password_attributes.xml");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(),
"$('gaia-signin').gaiaAuthHost_.samlHandler_"
".extractSamlPasswordAttributes = true;"));
base::Value attrs;
GetLoginUI()->RegisterMessageCallback("updatePasswordAttributes",
base::BindLambdaForTesting(
[&](const base::ListValue* val) { attrs = val->Clone(); }));
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
session_start_waiter.Wait();
ASSERT_TRUE(attrs.is_list());
ASSERT_EQ(3ul, attrs.GetList().size());
EXPECT_EQ("1550836258421", attrs.GetList()[0].GetString());
EXPECT_EQ("1551873058421", attrs.GetList()[1].GetString());
EXPECT_EQ("https://example.com/adfs/portal/updatepassword/",
attrs.GetList()[2].GetString());
}
class SAMLEnrollmentTest : public SamlTest {
public:
SAMLEnrollmentTest();
~SAMLEnrollmentTest() override;
// SamlTest:
void SetUpOnMainThread() override;
void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) override;
guest_view::TestGuestViewManager* GetGuestViewManager();
content::WebContents* GetEnrollmentContents();
protected:
LocalPolicyTestServerMixin local_policy_mixin_{&mixin_host_};
test::EnrollmentUIMixin enrollment_ui_{&mixin_host_};
guest_view::TestGuestViewManagerFactory guest_view_manager_factory_;
private:
DISALLOW_COPY_AND_ASSIGN(SAMLEnrollmentTest);
};
SAMLEnrollmentTest::SAMLEnrollmentTest() {
guest_view::GuestViewManager::set_factory_for_testing(
&guest_view_manager_factory_);
gaia_frame_parent_ = "oauth-enroll-auth-view";
authenticator_id_ = "$('oauth-enrollment').authenticator_";
}
SAMLEnrollmentTest::~SAMLEnrollmentTest() {}
void SAMLEnrollmentTest::SetUpOnMainThread() {
FakeGaia::AccessTokenInfo token_info;
token_info.token = kTestUserinfoToken;
token_info.scopes.insert(GaiaConstants::kDeviceManagementServiceOAuth);
token_info.scopes.insert(GaiaConstants::kOAuthWrapBridgeUserInfoScope);
token_info.audience = GaiaUrls::GetInstance()->oauth2_chrome_client_id();
token_info.email = kFirstSAMLUserEmail;
fake_gaia_.fake_gaia()->IssueOAuthToken(kTestRefreshToken, token_info);
SamlTest::SetUpOnMainThread();
}
void SAMLEnrollmentTest::StartSamlAndWaitForIdpPageLoad(
const std::string& gaia_email) {
WaitForSigninScreen();
ExistingUserController::current_controller()->OnStartEnterpriseEnrollment();
while (!GetEnrollmentContents()) {
GetGuestViewManager()->WaitForNextGuestCreated();
}
// Wait for Gaia is ready.
OobeBaseTest::WaitForGaiaPageEvent("backButton");
SigninFrameJS().TypeIntoPath(gaia_email, {"identifier"});
SigninFrameJS().TapOn("nextButton");
OobeBaseTest::WaitForGaiaPageEvent("authFlowChange");
}
guest_view::TestGuestViewManager* SAMLEnrollmentTest::GetGuestViewManager() {
using namespace guest_view;
return static_cast<TestGuestViewManager*>(
TestGuestViewManager::FromBrowserContext(
ProfileHelper::GetSigninProfile()));
}
content::WebContents* SAMLEnrollmentTest::GetEnrollmentContents() {
content::RenderFrameHost* frame_host =
signin::GetAuthFrame(GetLoginUI()->GetWebContents(), gaia_frame_parent_);
if (!frame_host)
return nullptr;
return content::WebContents::FromRenderFrameHost(frame_host);
}
IN_PROC_BROWSER_TEST_F(SAMLEnrollmentTest, WithoutCredentialsPassingAPI) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
SigninFrameJS().TapOn("Submit");
enrollment_ui_.WaitForStep(test::ui::kEnrollmentStepDeviceAttributes);
}
IN_PROC_BROWSER_TEST_F(SAMLEnrollmentTest, WithCredentialsPassingAPI) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_api_login.html");
fake_saml_idp()->SetLoginAuthHTMLTemplate("saml_api_login_auth.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
// Fill-in the SAML IdP form and submit.
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
SigninFrameJS().TapOn("Submit");
enrollment_ui_.WaitForStep(test::ui::kEnrollmentStepDeviceAttributes);
}
class SAMLPolicyTest : public SamlTest {
public:
SAMLPolicyTest();
~SAMLPolicyTest() override;
// SamlTest:
void SetUpInProcessBrowserTestFixture() override;
void SetUpOnMainThread() override;
void SetSAMLOfflineSigninTimeLimitPolicy(int limit);
void EnableTransferSAMLCookiesPolicy();
void SetLoginBehaviorPolicyToSAMLInterstitial();
void SetLoginVideoCaptureAllowedUrls(const std::vector<GURL>& allowed);
void ShowGAIALoginForm();
void ShowSAMLInterstitial();
void ClickNextOnSAMLInterstitialPage();
void ClickChangeAccountOnSAMLInterstitialPage();
void LogInWithSAML(const std::string& user_id,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie);
std::string GetCookieValue(const std::string& name);
void GetCookies();
protected:
policy::DevicePolicyCrosTestHelper test_helper_;
policy::DevicePolicyBuilder* device_policy_;
policy::MockConfigurationPolicyProvider provider_;
net::CookieList cookie_list_;
private:
DISALLOW_COPY_AND_ASSIGN(SAMLPolicyTest);
};
SAMLPolicyTest::SAMLPolicyTest()
: device_policy_(test_helper_.device_policy()) {}
SAMLPolicyTest::~SAMLPolicyTest() {}
void SAMLPolicyTest::SetUpInProcessBrowserTestFixture() {
SessionManagerClient::InitializeFakeInMemory();
SamlTest::SetUpInProcessBrowserTestFixture();
// Initialize device policy.
std::set<std::string> device_affiliation_ids;
device_affiliation_ids.insert(kAffiliationID);
auto affiliation_helper = policy::AffiliationTestHelper::CreateForCloud(
FakeSessionManagerClient::Get());
ASSERT_NO_FATAL_FAILURE((affiliation_helper.SetDeviceAffiliationIDs(
&test_helper_, device_affiliation_ids)));
// Initialize user policy.
EXPECT_CALL(provider_, IsInitializationComplete(_))
.WillRepeatedly(Return(true));
policy::BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
}
void SAMLPolicyTest::SetUpOnMainThread() {
SamlTest::SetUpOnMainThread();
// Pretend that the test users' OAuth tokens are valid.
user_manager::UserManager::Get()->SaveUserOAuthStatus(
AccountId::FromUserEmailGaiaId(kFirstSAMLUserEmail, kFirstSAMLUserGaiaId),
user_manager::User::OAUTH2_TOKEN_STATUS_VALID);
user_manager::UserManager::Get()->SaveUserOAuthStatus(
AccountId::FromUserEmailGaiaId(kNonSAMLUserEmail, kNonSAMLUserGaiaId),
user_manager::User::OAUTH2_TOKEN_STATUS_VALID);
user_manager::UserManager::Get()->SaveUserOAuthStatus(
AccountId::FromUserEmailGaiaId(kDifferentDomainSAMLUserEmail,
kDifferentDomainSAMLUserGaiaId),
user_manager::User::OAUTH2_TOKEN_STATUS_VALID);
// Give affiliated users appropriate affiliation IDs.
std::set<std::string> user_affiliation_ids;
user_affiliation_ids.insert(kAffiliationID);
chromeos::ChromeUserManager::Get()->SetUserAffiliation(
AccountId::FromUserEmailGaiaId(kFirstSAMLUserEmail, kFirstSAMLUserGaiaId),
user_affiliation_ids);
chromeos::ChromeUserManager::Get()->SetUserAffiliation(
AccountId::FromUserEmailGaiaId(kSecondSAMLUserEmail,
kSecondSAMLUserGaiaId),
user_affiliation_ids);
chromeos::ChromeUserManager::Get()->SetUserAffiliation(
AccountId::FromUserEmailGaiaId(kHTTPSAMLUserEmail, kHTTPSAMLUserGaiaId),
user_affiliation_ids);
chromeos::ChromeUserManager::Get()->SetUserAffiliation(
AccountId::FromUserEmailGaiaId(kNonSAMLUserEmail, kNonSAMLUserGaiaId),
user_affiliation_ids);
// Set up fake networks.
DBusThreadManager::Get()
->GetShillManagerClient()
->GetTestInterface()
->SetupDefaultEnvironment();
}
void SAMLPolicyTest::SetSAMLOfflineSigninTimeLimitPolicy(int limit) {
policy::PolicyMap user_policy;
user_policy.Set(policy::key::kSAMLOfflineSigninTimeLimit,
policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
policy::POLICY_SOURCE_CLOUD,
std::make_unique<base::Value>(limit), nullptr);
provider_.UpdateChromePolicy(user_policy);
base::RunLoop().RunUntilIdle();
}
void SAMLPolicyTest::EnableTransferSAMLCookiesPolicy() {
em::ChromeDeviceSettingsProto& proto(device_policy_->payload());
proto.mutable_saml_settings()->set_transfer_saml_cookies(true);
base::RunLoop run_loop;
std::unique_ptr<CrosSettings::ObserverSubscription> observer =
CrosSettings::Get()->AddSettingsObserver(kAccountsPrefTransferSAMLCookies,
run_loop.QuitClosure());
device_policy_->SetDefaultSigningKey();
device_policy_->Build();
FakeSessionManagerClient::Get()->set_device_policy(device_policy_->GetBlob());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
run_loop.Run();
}
void SAMLPolicyTest::SetLoginBehaviorPolicyToSAMLInterstitial() {
em::ChromeDeviceSettingsProto& proto(device_policy_->payload());
proto.mutable_login_authentication_behavior()
->set_login_authentication_behavior(
em::LoginAuthenticationBehaviorProto_LoginBehavior_SAML_INTERSTITIAL);
base::RunLoop run_loop;
std::unique_ptr<CrosSettings::ObserverSubscription> observer =
CrosSettings::Get()->AddSettingsObserver(kLoginAuthenticationBehavior,
run_loop.QuitClosure());
device_policy_->SetDefaultSigningKey();
device_policy_->Build();
FakeSessionManagerClient::Get()->set_device_policy(device_policy_->GetBlob());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
run_loop.Run();
}
void SAMLPolicyTest::SetLoginVideoCaptureAllowedUrls(
const std::vector<GURL>& allowed) {
em::ChromeDeviceSettingsProto& proto(device_policy_->payload());
for (const GURL& url : allowed)
proto.mutable_login_video_capture_allowed_urls()->add_urls(url.spec());
base::RunLoop run_loop;
std::unique_ptr<CrosSettings::ObserverSubscription> observer =
CrosSettings::Get()->AddSettingsObserver(kLoginVideoCaptureAllowedUrls,
run_loop.QuitClosure());
device_policy_->SetDefaultSigningKey();
device_policy_->Build();
FakeSessionManagerClient::Get()->set_device_policy(device_policy_->GetBlob());
FakeSessionManagerClient::Get()->OnPropertyChangeComplete(true);
run_loop.Run();
}
void SAMLPolicyTest::ShowGAIALoginForm() {
login_screen_load_observer_->Wait();
content::DOMMessageQueue message_queue;
ASSERT_TRUE(content::ExecuteScript(
GetLoginUI()->GetWebContents(),
"$('gaia-signin').gaiaAuthHost_.addEventListener('ready', function() {"
" window.domAutomationController.send('ready');"
"});"));
ASSERT_TRUE(test::LoginScreenTester().ClickAddUserButton());
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"ready\"");
}
void SAMLPolicyTest::ShowSAMLInterstitial() {
login_screen_load_observer_->Wait();
content::DOMMessageQueue message_queue;
ASSERT_TRUE(
content::ExecuteScript(GetLoginUI()->GetWebContents(),
"{"
" let notify = function() {"
" window.domAutomationController.send("
" 'samlInterstitialPageReady');"
" };"
" if($('gaia-signin')."
" samlInterstitialPageReady) {"
" window.setTimeout(notify,0);"
" } else {"
" $('saml-interstitial').addEventListener("
" 'samlInterstitialPageReady', notify);"
" }"
"}"));
ASSERT_TRUE(test::LoginScreenTester().ClickAddUserButton());
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"samlInterstitialPageReady\"");
}
void SAMLPolicyTest::ClickNextOnSAMLInterstitialPage() {
login_screen_load_observer_->Wait();
content::DOMMessageQueue message_queue;
SetupAuthFlowChangeListener();
ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(),
"$('saml-interstitial').submit();"));
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"SamlLoaded\"");
}
void SAMLPolicyTest::ClickChangeAccountOnSAMLInterstitialPage() {
login_screen_load_observer_->Wait();
content::DOMMessageQueue message_queue;
ASSERT_TRUE(content::ExecuteScript(
GetLoginUI()->GetWebContents(),
"$('gaia-signin').gaiaAuthHost_.addEventListener('ready', function() {"
" window.domAutomationController.send('ready');"
"});"
"$('saml-interstitial').changeAccountLink.click();"));
std::string message;
do {
ASSERT_TRUE(message_queue.WaitForMessage(&message));
} while (message != "\"ready\"");
}
void SAMLPolicyTest::LogInWithSAML(const std::string& user_id,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(user_id);
fake_gaia_.fake_gaia()->SetFakeMergeSessionParams(user_id, auth_sid_cookie,
auth_lsid_cookie);
fake_gaia_.SetupFakeGaiaForLogin(user_id, "", kTestRefreshToken);
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
// Scraping a single password should finish the login right away.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
session_start_waiter.Wait();
}
std::string SAMLPolicyTest::GetCookieValue(const std::string& name) {
for (net::CookieList::const_iterator it = cookie_list_.begin();
it != cookie_list_.end(); ++it) {
if (it->Name() == name)
return it->Value();
}
return std::string();
}
void SAMLPolicyTest::GetCookies() {
Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUserUnsafe(
user_manager::UserManager::Get()->GetActiveUser());
ASSERT_TRUE(profile);
base::RunLoop run_loop;
network::mojom::CookieManagerPtr cookie_manager;
content::BrowserContext::GetDefaultStoragePartition(profile)
->GetCookieManagerForBrowserProcess()
->GetAllCookies(base::BindLambdaForTesting(
[&](const std::vector<net::CanonicalCookie>& cookies) {
cookie_list_ = cookies;
run_loop.Quit();
}));
run_loop.Run();
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_NoSAML) {
// Set the offline login time limit for SAML users to zero.
SetSAMLOfflineSigninTimeLimitPolicy(0);
WaitForSigninScreen();
fake_gaia_.fake_gaia()->SetFakeMergeSessionParams(
kNonSAMLUserEmail, FakeGaiaMixin::kFakeSIDCookie,
FakeGaiaMixin::kFakeLSIDCookie);
fake_gaia_.SetupFakeGaiaForLogin(kNonSAMLUserEmail, "", kTestRefreshToken);
// Log in without SAML.
LoginDisplayHost::default_host()
->GetOobeUI()
->GetGaiaScreenView()
->ShowSigninScreenForTest(kNonSAMLUserEmail, "password", "[]");
content::WindowedNotificationObserver(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources())
.Wait();
}
// Verifies that the offline login time limit does not affect a user who
// authenticated without SAML.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, NoSAML) {
login_screen_load_observer_->Wait();
// Verify that offline login is allowed.
test::OobeJS().ExpectTrue(
"window.getComputedStyle(document.querySelector("
" '#pod-row .reauth-hint-container')).display == 'none'");
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_SAMLNoLimit) {
// Remove the offline login time limit for SAML users.
SetSAMLOfflineSigninTimeLimitPolicy(-1);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
}
// Verifies that when no offline login time limit is set, a user who
// authenticated with SAML is allowed to log in offline.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, SAMLNoLimit) {
login_screen_load_observer_->Wait();
// Verify that offline login is allowed.
test::OobeJS().ExpectTrue(
"window.getComputedStyle(document.querySelector("
" '#pod-row .reauth-hint-container')).display == 'none'");
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_SAMLZeroLimit) {
// Set the offline login time limit for SAML users to zero.
SetSAMLOfflineSigninTimeLimitPolicy(0);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
}
// Verifies that when the offline login time limit is exceeded for a user who
// authenticated via SAML, that user is forced to log in online the next time.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, SAMLZeroLimit) {
login_screen_load_observer_->Wait();
// Verify that offline login is not allowed.
test::OobeJS().ExpectTrue(
"window.getComputedStyle(document.querySelector("
" '#pod-row .reauth-hint-container')).display != 'none'");
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_PRE_TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that when the DeviceTransferSAMLCookies policy is not enabled, SAML
// IdP cookies are not transferred to a user's profile on subsequent login, even
// if the user belongs to the domain that the device is enrolled into. Also
// verifies that GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie2, kTestAuthLSIDCookie2);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that when the DeviceTransferSAMLCookies policy is enabled, SAML IdP
// cookies are transferred to a user's profile on subsequent login when the user
// belongs to the domain that the device is enrolled into. Also verifies that
// GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
EnableTransferSAMLCookiesPolicy();
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie2, kTestAuthLSIDCookie2);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue2, GetCookieValue(kSAMLIdPCookieName));
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_TransferCookiesUnaffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kDifferentDomainSAMLUserEmail, kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that even if the DeviceTransferSAMLCookies policy is enabled, SAML
// IdP are not transferred to a user's profile on subsequent login if the user
// does not belong to the domain that the device is enrolled into. Also verifies
// that GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, TransferCookiesUnaffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
EnableTransferSAMLCookiesPolicy();
LogInWithSAML(kDifferentDomainSAMLUserEmail, kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Tests that the SAML interstitial page is loaded when the authentication
// behavior device policy is set to SAML_INTERSTITIAL, and when the user clicks
// the "change account" link, we go back to the default GAIA signin screen.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, SAMLInterstitialChangeAccount) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
SetLoginBehaviorPolicyToSAMLInterstitial();
WaitForSigninScreen();
ShowSAMLInterstitial();
test::OobeJS().ExpectHidden("signin-frame");
test::OobeJS().ExpectHidden("offline-gaia");
test::OobeJS().ExpectVisible("saml-interstitial");
// Click the "change account" link on the SAML interstitial page.
ClickChangeAccountOnSAMLInterstitialPage();
// Expects that only the gaia signin frame is visible and shown.
test::OobeJS().ExpectHasClass("show", {"signin-frame"});
test::OobeJS().ExpectVisible("signin-frame");
test::OobeJS().ExpectHidden("offline-gaia");
test::OobeJS().ExpectHidden("saml-interstitial");
}
// Tests that clicking "Next" in the SAML interstitial page successfully
// triggers a SAML redirect request, and the SAML IdP authentication page is
// loaded and authenticaing there is successful.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, SAMLInterstitialNext) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
fake_gaia_.fake_gaia()->SetFakeMergeSessionParams(
kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
SetLoginBehaviorPolicyToSAMLInterstitial();
WaitForSigninScreen();
ShowSAMLInterstitial();
ClickNextOnSAMLInterstitialPage();
SigninFrameJS().TypeIntoPath("fake_user", {"Email"});
SigninFrameJS().TypeIntoPath("fake_password", {"Password"});
// Scraping one password should finish login.
content::WindowedNotificationObserver session_start_waiter(
chrome::NOTIFICATION_SESSION_STARTED,
content::NotificationService::AllSources());
SigninFrameJS().TapOn("Submit");
session_start_waiter.Wait();
}
// A specialization of SAMLPolicyTest which doesn't pass the command-line switch
// forcing the WebUI login, thus allowing views-based login.
class SAMLPolicyViewsBasedLoginTest : public SAMLPolicyTest {
public:
SAMLPolicyViewsBasedLoginTest() = default;
~SAMLPolicyViewsBasedLoginTest() override = default;
protected:
// OobeBaseTest:
bool ShouldForceWebUiLogin() override {
// Allow the Views-based login to be used.
return false;
}
private:
DISALLOW_COPY_AND_ASSIGN(SAMLPolicyViewsBasedLoginTest);
};
IN_PROC_BROWSER_TEST_F(SAMLPolicyViewsBasedLoginTest,
PRE_TestLoginMediaPermission) {
// Mark OOBE completed to go directly to the sign-in screen - this is
// currently needed to trigger the views-based login UI.
StartupUtils::MarkOobeCompleted();
}
// Ensure that the permission status of getUserMedia requests from SAML login
// pages is controlled by the kLoginVideoCaptureAllowedUrls pref rather than the
// underlying user content setting.
IN_PROC_BROWSER_TEST_F(SAMLPolicyViewsBasedLoginTest,
TestLoginMediaPermission) {
EXPECT_TRUE(ash::features::IsViewsLoginEnabled());
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
const GURL url1("https://google.com");
const GURL url2("https://corp.example.com");
const GURL url3("https://not-allowed.com");
SetLoginVideoCaptureAllowedUrls({url1, url2});
WaitForSigninScreen();
content::WebContents* web_contents = GetLoginUI()->GetWebContents();
content::WebContentsDelegate* web_contents_delegate =
web_contents->GetDelegate();
// Mic should always be blocked.
EXPECT_FALSE(web_contents_delegate->CheckMediaAccessPermission(
web_contents->GetMainFrame(), url1, blink::MEDIA_DEVICE_AUDIO_CAPTURE));
// Camera should be allowed if allowed by the whitelist, otherwise blocked.
EXPECT_TRUE(web_contents_delegate->CheckMediaAccessPermission(
web_contents->GetMainFrame(), url1, blink::MEDIA_DEVICE_VIDEO_CAPTURE));
EXPECT_TRUE(web_contents_delegate->CheckMediaAccessPermission(
web_contents->GetMainFrame(), url2, blink::MEDIA_DEVICE_VIDEO_CAPTURE));
EXPECT_FALSE(web_contents_delegate->CheckMediaAccessPermission(
web_contents->GetMainFrame(), url3, blink::MEDIA_DEVICE_VIDEO_CAPTURE));
// Camera should be blocked in the login screen, even if it's allowed via
// content setting.
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
HostContentSettingsMapFactory::GetForProfile(profile)
->SetContentSettingDefaultScope(url3, url3,
CONTENT_SETTINGS_TYPE_MEDIASTREAM_CAMERA,
std::string(), CONTENT_SETTING_ALLOW);
EXPECT_FALSE(web_contents_delegate->CheckMediaAccessPermission(
web_contents->GetMainFrame(), url3, blink::MEDIA_DEVICE_VIDEO_CAPTURE));
}
} // namespace chromeos