summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Xie <dxie@google.com>2015-01-07 13:36:49 -0800
committerDaniel Xie <dxie@google.com>2015-01-07 21:39:06 +0000
commit4e24d622193b4d1ba17e34ded9c99f0696bd2903 (patch)
tree955a476fe3b4df7f6d50c276c5aeaa832a1ae74a
parentd979fae3a86c2b6f4a7a7f94ae6bd6658e86bacc (diff)
downloadchromium_src-4e24d622193b4d1ba17e34ded9c99f0696bd2903.zip
chromium_src-4e24d622193b4d1ba17e34ded9c99f0696bd2903.tar.gz
chromium_src-4e24d622193b4d1ba17e34ded9c99f0696bd2903.tar.bz2
Fix Chrome OS enrollment with SAML accounts
CL 677703002 caused a regression in the Chrome OS enrollment flow: If the user authenticates via SAML and the credentials passing API is not used, the enrollment flow will get stuck. This happens because the GAIA auth extension wants to proceed with scraped password verification but enrollment does not need the password and does not implement the verification step. This CL fixes enrollment by skipping password verification for enrollment. The CL also adds a regression test - the first UI-driven end-to-end enrollment test AFAICT. BUG=438471 TEST=New browser test Review URL: https://codereview.chromium.org/781623003 (cherry picked from commit 9ebcd79ec2de9af9b2855bf3a901d138f3af2267) Cr-Original-Commit-Position: refs/heads/master@{#308374} Cr-Commit-Position: refs/branch-heads/2214@{#399} Cr-Branched-From: 03655fd3f6d72165dc3c9bd2c89807305316fe6c-refs/heads/master@{#303346}
-rw-r--r--chrome/browser/chromeos/login/saml/saml_browsertest.cc191
-rw-r--r--chrome/browser/policy/test/local_policy_test_server.cc10
-rw-r--r--chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js1
-rw-r--r--chrome/browser/resources/gaia_auth/main.js9
-rw-r--r--chrome/browser/resources/gaia_auth_host/gaia_auth_host.js1
5 files changed, 199 insertions, 13 deletions
diff --git a/chrome/browser/chromeos/login/saml/saml_browsertest.cc b/chrome/browser/chromeos/login/saml/saml_browsertest.cc
index 44e8402..523b555 100644
--- a/chrome/browser/chromeos/login/saml/saml_browsertest.cc
+++ b/chrome/browser/chromeos/login/saml/saml_browsertest.cc
@@ -2,13 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <cstring>
+
#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/files/scoped_temp_dir.h"
#include "base/location.h"
+#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_ptr.h"
#include "base/path_service.h"
@@ -31,6 +35,7 @@
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/chromeos/settings/cros_settings.h"
#include "chrome/browser/lifetime/application_lifetime.h"
+#include "chrome/browser/policy/test/local_policy_test_server.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/signin/inline_login_ui.h"
#include "chrome/common/chrome_paths.h"
@@ -45,15 +50,20 @@
#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/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_thread.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 "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.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_monster.h"
@@ -103,6 +113,10 @@ const char kSAMLIdPCookieValue2[] = "value-2";
const char kRelayState[] = "RelayState";
+const char kTestUserinfoToken[] = "fake-userinfo-token";
+const char kTestRefreshToken[] = "fake-refresh-token";
+const char kPolicy[] = "{\"managed_users\": [\"*\"]}";
+
// 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
@@ -242,7 +256,7 @@ scoped_ptr<HttpResponse> FakeSamlIdp::BuildHTMLResponse(
class SamlTest : public InProcessBrowserTest {
public:
- SamlTest() : saml_load_injected_(false) {}
+ SamlTest() : gaia_frame_parent_("signin-frame"), saml_load_injected_(false) {}
virtual ~SamlTest() {}
virtual void SetUp() override {
@@ -340,7 +354,7 @@ class SamlTest : public InProcessBrowserTest {
login_screen_load_observer_->Wait();
}
- void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) {
+ virtual void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) {
WaitForSigninScreen();
if (!saml_load_injected_) {
@@ -415,7 +429,7 @@ class SamlTest : public InProcessBrowserTest {
// Executes JavaScript code in the auth iframe hosted by gaia_auth extension.
void ExecuteJsInSigninFrame(const std::string& js) {
content::RenderFrameHost* frame = InlineLoginUI::GetAuthIframe(
- GetLoginUI()->GetWebContents(), GURL(), "signin-frame");
+ GetLoginUI()->GetWebContents(), GURL(), gaia_frame_parent_);
ASSERT_TRUE(content::ExecuteScript(frame, js));
}
@@ -425,11 +439,14 @@ class SamlTest : public InProcessBrowserTest {
scoped_ptr<content::WindowedNotificationObserver> login_screen_load_observer_;
FakeGaia fake_gaia_;
- private:
- FakeSamlIdp fake_saml_idp_;
+ std::string gaia_frame_parent_;
+
scoped_ptr<HTTPSForwarder> gaia_https_forwarder_;
scoped_ptr<HTTPSForwarder> saml_https_forwarder_;
+ private:
+ FakeSamlIdp fake_saml_idp_;
+
bool saml_load_injected_;
DISALLOW_COPY_AND_ASSIGN(SamlTest);
@@ -639,6 +656,170 @@ IN_PROC_BROWSER_TEST_F(SamlTest, MetaRefreshToHTTPDisallowed) {
WaitForAndGetFatalErrorMessage());
}
+class SAMLEnrollmentTest : public SamlTest,
+ public content::WebContentsObserver {
+ public:
+ SAMLEnrollmentTest();
+ ~SAMLEnrollmentTest() override;
+
+ // SamlTest:
+ void SetUp() override;
+ void SetUpCommandLine(CommandLine* command_line) override;
+ void SetUpOnMainThread() override;
+ void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) override;
+
+ // content::WebContentsObserver:
+ void RenderFrameCreated(content::RenderFrameHost* render_frame_host) override;
+ void DidFinishLoad(content::RenderFrameHost* render_frame_host,
+ const GURL& validated_url) override;
+
+ void WaitForEnrollmentSuccess();
+
+ private:
+ scoped_ptr<policy::LocalPolicyTestServer> test_server_;
+ base::ScopedTempDir temp_dir_;
+
+ scoped_ptr<base::RunLoop> run_loop_;
+ content::RenderFrameHost* auth_frame_;
+
+ DISALLOW_COPY_AND_ASSIGN(SAMLEnrollmentTest);
+};
+
+SAMLEnrollmentTest::SAMLEnrollmentTest() : auth_frame_(nullptr) {
+ gaia_frame_parent_ = "oauth-enroll-signin-frame";
+}
+
+SAMLEnrollmentTest::~SAMLEnrollmentTest() {
+}
+
+void SAMLEnrollmentTest::SetUp() {
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ const base::FilePath policy_file =
+ temp_dir_.path().AppendASCII("policy.json");
+ ASSERT_EQ(static_cast<int>(strlen(kPolicy)),
+ base::WriteFile(policy_file, kPolicy, strlen(kPolicy)));
+
+ test_server_.reset(new policy::LocalPolicyTestServer(policy_file));
+ ASSERT_TRUE(test_server_->Start());
+
+ SamlTest::SetUp();
+}
+
+void SAMLEnrollmentTest::SetUpCommandLine(CommandLine* command_line) {
+ command_line->AppendSwitchASCII(policy::switches::kDeviceManagementUrl,
+ test_server_->GetServiceURL().spec());
+ command_line->AppendSwitch(policy::switches::kDisablePolicyKeyVerification);
+ command_line->AppendSwitch(switches::kEnterpriseEnrollmentSkipRobotAuth);
+
+ SamlTest::SetUpCommandLine(command_line);
+}
+
+void SAMLEnrollmentTest::SetUpOnMainThread() {
+ Observe(GetLoginUI()->GetWebContents());
+
+ 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_.IssueOAuthToken(kTestRefreshToken, token_info);
+
+ SamlTest::SetUpOnMainThread();
+}
+
+void SAMLEnrollmentTest::StartSamlAndWaitForIdpPageLoad(
+ const std::string& gaia_email) {
+ WaitForSigninScreen();
+ run_loop_.reset(new base::RunLoop);
+ ExistingUserController::current_controller()->OnStartEnterpriseEnrollment();
+ run_loop_->Run();
+
+ SetSignFormField("Email", gaia_email);
+
+ run_loop_.reset(new base::RunLoop);
+ ExecuteJsInSigninFrame("document.getElementById('signIn').click();");
+ run_loop_->Run();
+}
+
+void SAMLEnrollmentTest::RenderFrameCreated(
+ content::RenderFrameHost* render_frame_host) {
+ content::RenderFrameHost* parent = render_frame_host->GetParent();
+ if (!parent || parent->GetFrameName() != gaia_frame_parent_)
+ return;
+
+ // The GAIA extension created the iframe in which the login form will be
+ // shown. Now wait for the login form to finish loading.
+ auth_frame_ = render_frame_host;
+ Observe(content::WebContents::FromRenderFrameHost(auth_frame_));
+}
+
+void SAMLEnrollmentTest::DidFinishLoad(
+ content::RenderFrameHost* render_frame_host,
+ const GURL& validated_url) {
+ if (render_frame_host != auth_frame_)
+ return;
+
+ const GURL origin = validated_url.GetOrigin();
+ if (origin != gaia_https_forwarder_->GetURL("") &&
+ origin != saml_https_forwarder_->GetURL("")) {
+ return;
+ }
+
+ // The GAIA or SAML IdP login form finished loading.
+ if (run_loop_)
+ run_loop_->Quit();
+}
+
+// Waits until the class |oauth-enroll-state-success| becomes set for the
+// enrollment screen, indicating enrollment success.
+void SAMLEnrollmentTest::WaitForEnrollmentSuccess() {
+ bool done = false;
+ ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
+ GetLoginUI()->GetWebContents(),
+ "var enrollmentScreen = document.getElementById('oauth-enrollment');"
+ "function SendReplyIfEnrollmentDone() {"
+ " if (!enrollmentScreen.classList.contains("
+ " 'oauth-enroll-state-success')) {"
+ " return false;"
+ " }"
+ " domAutomationController.send(true);"
+ " observer.disconnect();"
+ " return true;"
+ "}"
+ "var observer = new MutationObserver(SendReplyIfEnrollmentDone);"
+ "if (!SendReplyIfEnrollmentDone()) {"
+ " var options = { attributes: true, attributeFilter: [ 'class' ] };"
+ " observer.observe(enrollmentScreen, options);"
+ "}",
+ &done));
+}
+
+IN_PROC_BROWSER_TEST_F(SAMLEnrollmentTest, WithoutCredentialsPassingAPI) {
+ fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
+ StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
+
+ // Fill-in the SAML IdP form and submit.
+ SetSignFormField("Email", "fake_user");
+ SetSignFormField("Password", "fake_password");
+ ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
+
+ WaitForEnrollmentSuccess();
+}
+
+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.
+ SetSignFormField("Email", "fake_user");
+ SetSignFormField("Password", "fake_password");
+ ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
+
+ WaitForEnrollmentSuccess();
+}
+
class SAMLPolicyTest : public SamlTest {
public:
SAMLPolicyTest();
diff --git a/chrome/browser/policy/test/local_policy_test_server.cc b/chrome/browser/policy/test/local_policy_test_server.cc
index e4c73f0..d4bb6c7 100644
--- a/chrome/browser/policy/test/local_policy_test_server.cc
+++ b/chrome/browser/policy/test/local_policy_test_server.cc
@@ -197,21 +197,15 @@ bool LocalPolicyTestServer::SetPythonPath() const {
}
AppendToPythonPath(pyproto_dir
- .AppendASCII("chrome")
- .AppendASCII("browser")
- .AppendASCII("policy")
- .AppendASCII("proto")
- .AppendASCII("cloud"));
- AppendToPythonPath(pyproto_dir
.AppendASCII("policy")
.AppendASCII("proto"));
#if defined(OS_CHROMEOS)
AppendToPythonPath(pyproto_dir
.AppendASCII("chrome")
.AppendASCII("browser")
+ .AppendASCII("chromeos")
.AppendASCII("policy")
- .AppendASCII("proto")
- .AppendASCII("chromeos"));
+ .AppendASCII("proto"));
#endif
return true;
diff --git a/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js b/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
index 7660fc9..f9f7d0c 100644
--- a/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
+++ b/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
@@ -151,6 +151,7 @@ login.createScreen('OAuthEnrollmentScreen', 'oauth-enrollment', function() {
onBeforeShow: function(data) {
var url = data.signin_url;
url += '?gaiaUrl=' + encodeURIComponent(data.gaiaUrl);
+ url += '&needPassword=0';
this.signInUrl_ = url;
var modes = ['manual', 'forced', 'auto'];
for (var i = 0; i < modes.length; ++i) {
diff --git a/chrome/browser/resources/gaia_auth/main.js b/chrome/browser/resources/gaia_auth/main.js
index 05271fb..97107f5 100644
--- a/chrome/browser/resources/gaia_auth/main.js
+++ b/chrome/browser/resources/gaia_auth/main.js
@@ -57,6 +57,7 @@ Authenticator.prototype = {
// when support for key types other than plain text password is added.
passwordBytes_: null,
+ needPassword_: false,
chooseWhatToSync_: false,
skipForNow_: false,
sessionIndex_: null,
@@ -90,6 +91,7 @@ Authenticator.prototype = {
this.isConstrainedWindow_ = params.constrained == '1';
this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
+ this.needPassword_ = params.needPassword == '1';
// For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
// 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
@@ -377,8 +379,15 @@ Authenticator.prototype = {
this.sessionIndex_ = msg.sessionIndex;
if (this.passwordBytes_) {
+ // If the credentials passing API was used, login is complete.
window.parent.postMessage({method: 'samlApiUsed'}, this.parentPage_);
this.completeLogin_(msg);
+ } else if (!this.needPassword_) {
+ // If the credentials passing API was not used, the password was obtained
+ // by scraping. It must be verified before use. However, the host may not
+ // be interested in the password at all. In that case, verification is
+ // unnecessary and login is complete.
+ this.completeLogin_(msg);
} else {
this.supportChannel_.sendWithCallback(
{name: 'getScrapedPasswords'},
diff --git a/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js b/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
index cf870cb..5aefaf7 100644
--- a/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
+++ b/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
@@ -253,6 +253,7 @@ cr.define('cr.login', function() {
populateParams(SUPPORTED_PARAMS, data);
populateParams(LOCALIZED_STRING_PARAMS, data.localizedStrings);
params.push('parentPage=' + encodeURIComponent(window.location.origin));
+ params.push('needPassword=1');
var url;
switch (authMode) {