1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "base/command_line.h"
6 #include "base/file_util.h"
7 #include "base/files/file_path.h"
8 #include "base/path_service.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/chrome_notification_types.h"
12 #include "chrome/browser/chromeos/login/existing_user_controller.h"
13 #include "chrome/browser/chromeos/login/login_display_host_impl.h"
14 #include "chrome/browser/chromeos/login/test/oobe_screen_waiter.h"
15 #include "chrome/browser/chromeos/login/user.h"
16 #include "chrome/browser/chromeos/login/user_manager.h"
17 #include "chrome/browser/chromeos/login/webui_login_display.h"
18 #include "chrome/browser/chromeos/login/wizard_controller.h"
19 #include "chrome/browser/lifetime/application_lifetime.h"
20 #include "chrome/common/chrome_paths.h"
21 #include "chrome/common/chrome_switches.h"
22 #include "chrome/test/base/in_process_browser_test.h"
23 #include "chromeos/chromeos_switches.h"
24 #include "content/public/browser/render_view_host.h"
25 #include "content/public/browser/web_contents.h"
26 #include "content/public/test/browser_test_utils.h"
27 #include "content/public/test/test_utils.h"
28 #include "google_apis/gaia/fake_gaia.h"
29 #include "google_apis/gaia/gaia_switches.h"
30 #include "net/base/url_util.h"
31 #include "net/dns/mock_host_resolver.h"
32 #include "net/test/embedded_test_server/embedded_test_server.h"
33 #include "net/test/embedded_test_server/http_request.h"
34 #include "net/test/embedded_test_server/http_response.h"
35 #include "testing/gtest/include/gtest/gtest.h"
37 using net::test_server::BasicHttpResponse
;
38 using net::test_server::HttpRequest
;
39 using net::test_server::HttpResponse
;
45 const char kTestAuthSIDCookie
[] = "fake-auth-SID-cookie";
46 const char kTestAuthLSIDCookie
[] = "fake-auth-LSID-cookie";
47 const char kTestAuthCode
[] = "fake-auth-code";
48 const char kTestGaiaUberToken
[] = "fake-uber-token";
49 const char kTestAuthLoginAccessToken
[] = "fake-access-token";
50 const char kTestRefreshToken
[] = "fake-refresh-token";
51 const char kTestSessionSIDCookie
[] = "fake-session-SID-cookie";
52 const char kTestSessionLSIDCookie
[] = "fake-session-LSID-cookie";
54 const char kAnotherUserEmail
[] = "alice@example.com";
55 const char kUserEmail
[] = "bob@example.com";
57 const char kRelayState
[] = "RelayState";
59 // FakeSamlIdp serves IdP auth form and the form submission. The form is
60 // served with the template's RelayState placeholder expanded to the real
61 // RelayState parameter from request. The form submission redirects back to
62 // FakeGaia with the same RelayState.
68 void SetUp(const std::string
& base_path
, const GURL
& gaia_url
);
70 void SetLoginHTMLTemplate(const std::string
& template_file
);
71 void SetLoginAuthHTMLTemplate(const std::string
& template_file
);
73 scoped_ptr
<HttpResponse
> HandleRequest(const HttpRequest
& request
);
76 scoped_ptr
<HttpResponse
> BuildHTMLResponse(const std::string
& html_template
,
77 const std::string
& relay_state
,
78 const std::string
& next_path
);
80 base::FilePath html_template_dir_
;
82 std::string login_path_
;
83 std::string login_auth_path_
;
85 std::string login_html_template_
;
86 std::string login_auth_html_template_
;
87 GURL gaia_assertion_url_
;
89 DISALLOW_COPY_AND_ASSIGN(FakeSamlIdp
);
92 FakeSamlIdp::FakeSamlIdp() {
95 FakeSamlIdp::~FakeSamlIdp() {
98 void FakeSamlIdp::SetUp(const std::string
& base_path
, const GURL
& gaia_url
) {
99 base::FilePath test_data_dir
;
100 ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA
, &test_data_dir
));
101 html_template_dir_
= test_data_dir
.Append("login");
103 login_path_
= base_path
;
104 login_auth_path_
= base_path
+ "Auth";
105 gaia_assertion_url_
= gaia_url
.Resolve("/SSO");
108 void FakeSamlIdp::SetLoginHTMLTemplate(const std::string
& template_file
) {
109 EXPECT_TRUE(base::ReadFileToString(
110 html_template_dir_
.Append(template_file
),
111 &login_html_template_
));
114 void FakeSamlIdp::SetLoginAuthHTMLTemplate(const std::string
& template_file
) {
115 EXPECT_TRUE(base::ReadFileToString(
116 html_template_dir_
.Append(template_file
),
117 &login_auth_html_template_
));
120 scoped_ptr
<HttpResponse
> FakeSamlIdp::HandleRequest(
121 const HttpRequest
& request
) {
122 // The scheme and host of the URL is actually not important but required to
123 // get a valid GURL in order to parse |request.relative_url|.
124 GURL request_url
= GURL("http://localhost").Resolve(request
.relative_url
);
125 std::string request_path
= request_url
.path();
127 if (request_path
== login_path_
) {
128 std::string relay_state
;
129 net::GetValueForKeyInQuery(request_url
, kRelayState
, &relay_state
);
130 return BuildHTMLResponse(login_html_template_
,
135 if (request_path
!= login_auth_path_
) {
136 // Request not understood.
137 return scoped_ptr
<HttpResponse
>();
140 std::string relay_state
;
141 FakeGaia::GetQueryParameter(request
.content
, kRelayState
, &relay_state
);
142 GURL redirect_url
= gaia_assertion_url_
;
144 if (!login_auth_html_template_
.empty()) {
145 return BuildHTMLResponse(login_auth_html_template_
,
147 redirect_url
.spec());
150 redirect_url
= net::AppendQueryParameter(
151 redirect_url
, "SAMLResponse", "fake_response");
152 redirect_url
= net::AppendQueryParameter(
153 redirect_url
, kRelayState
, relay_state
);
155 scoped_ptr
<BasicHttpResponse
> http_response(new BasicHttpResponse());
156 http_response
->set_code(net::HTTP_TEMPORARY_REDIRECT
);
157 http_response
->AddCustomHeader("Location", redirect_url
.spec());
158 return http_response
.PassAs
<HttpResponse
>();
161 scoped_ptr
<HttpResponse
> FakeSamlIdp::BuildHTMLResponse(
162 const std::string
& html_template
,
163 const std::string
& relay_state
,
164 const std::string
& next_path
) {
165 std::string response_html
= html_template
;
166 ReplaceSubstringsAfterOffset(&response_html
, 0, "$RelayState", relay_state
);
167 ReplaceSubstringsAfterOffset(&response_html
, 0, "$Post", next_path
);
169 scoped_ptr
<BasicHttpResponse
> http_response(new BasicHttpResponse());
170 http_response
->set_code(net::HTTP_OK
);
171 http_response
->set_content(response_html
);
172 http_response
->set_content_type("text/html");
174 return http_response
.PassAs
<HttpResponse
>();
179 class SamlTest
: public InProcessBrowserTest
{
181 SamlTest() : saml_load_injected_(false) {}
182 virtual ~SamlTest() {}
184 virtual void SetUp() OVERRIDE
{
185 // Start embedded test server here so that we can get server base url
186 // and override Gaia urls in SetupCommandLine.
187 ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
189 // Stop IO thread here because no threads are allowed while
190 // spawning sandbox host process. See crbug.com/322732.
191 embedded_test_server()->StopThread();
193 InProcessBrowserTest::SetUp();
196 virtual void SetUpInProcessBrowserTestFixture() OVERRIDE
{
197 host_resolver()->AddRule("*", "127.0.0.1");
200 virtual void SetUpCommandLine(CommandLine
* command_line
) OVERRIDE
{
201 command_line
->AppendSwitch(switches::kLoginManager
);
202 command_line
->AppendSwitch(switches::kForceLoginManagerInTests
);
203 command_line
->AppendSwitch(::switches::kDisableBackgroundNetworking
);
204 command_line
->AppendSwitchASCII(switches::kLoginProfile
, "user");
205 command_line
->AppendSwitch(switches::kEnableSamlSignin
);
207 const GURL
& server_url
= embedded_test_server()->base_url();
209 std::string
gaia_host("gaia");
210 GURL::Replacements replace_gaia_host
;
211 replace_gaia_host
.SetHostStr(gaia_host
);
212 gaia_url_
= server_url
.ReplaceComponents(replace_gaia_host
);
214 command_line
->AppendSwitchASCII(::switches::kGaiaUrl
, gaia_url_
.spec());
215 command_line
->AppendSwitchASCII(::switches::kLsoUrl
, gaia_url_
.spec());
216 command_line
->AppendSwitchASCII(::switches::kGoogleApisUrl
,
218 fake_gaia_
.Initialize();
220 std::string
saml_idp_host("saml.idp");
221 GURL::Replacements replace_saml_idp_host
;
222 replace_saml_idp_host
.SetHostStr(saml_idp_host
);
223 GURL saml_idp_url
= server_url
.ReplaceComponents(replace_saml_idp_host
);
224 saml_idp_url
= saml_idp_url
.Resolve("/SAML/SSO");
226 fake_saml_idp_
.SetUp(saml_idp_url
.path(), gaia_url_
);
227 fake_gaia_
.RegisterSamlUser(kAnotherUserEmail
, saml_idp_url
);
228 fake_gaia_
.RegisterSamlUser(kUserEmail
, saml_idp_url
);
231 virtual void SetUpOnMainThread() OVERRIDE
{
232 FakeGaia::MergeSessionParams params
;
233 params
.auth_sid_cookie
= kTestAuthSIDCookie
;
234 params
.auth_lsid_cookie
= kTestAuthLSIDCookie
;
235 params
.auth_code
= kTestAuthCode
;
236 params
.refresh_token
= kTestRefreshToken
;
237 params
.access_token
= kTestAuthLoginAccessToken
;
238 params
.gaia_uber_token
= kTestGaiaUberToken
;
239 params
.session_sid_cookie
= kTestSessionSIDCookie
;
240 params
.session_lsid_cookie
= kTestSessionLSIDCookie
;
241 params
.email
= kUserEmail
;
242 fake_gaia_
.SetMergeSessionParams(params
);
244 embedded_test_server()->RegisterRequestHandler(
245 base::Bind(&FakeGaia::HandleRequest
, base::Unretained(&fake_gaia_
)));
246 embedded_test_server()->RegisterRequestHandler(base::Bind(
247 &FakeSamlIdp::HandleRequest
, base::Unretained(&fake_saml_idp_
)));
249 // Restart the thread as the sandbox host process has already been spawned.
250 embedded_test_server()->RestartThreadAndListen();
253 virtual void CleanUpOnMainThread() OVERRIDE
{
254 // If the login display is still showing, exit gracefully.
255 if (LoginDisplayHostImpl::default_host()) {
256 base::MessageLoop::current()->PostTask(FROM_HERE
,
257 base::Bind(&chrome::AttemptExit
));
258 content::RunMessageLoop();
262 WebUILoginDisplay
* GetLoginDisplay() {
263 ExistingUserController
* controller
=
264 ExistingUserController::current_controller();
266 return static_cast<WebUILoginDisplay
*>(controller
->login_display());
269 void WaitForSigninScreen() {
270 WizardController::SkipPostLoginScreensForTesting();
271 WizardController
* wizard_controller
=
272 chromeos::WizardController::default_controller();
273 CHECK(wizard_controller
);
274 wizard_controller
->SkipToLoginForTesting(LoginScreenContext());
276 content::WindowedNotificationObserver(
277 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE
,
278 content::NotificationService::AllSources()).Wait();
281 void StartSamlAndWaitForIdpPageLoad(const std::string
& gaia_email
) {
282 WaitForSigninScreen();
284 if (!saml_load_injected_
) {
285 saml_load_injected_
= true;
287 ASSERT_TRUE(content::ExecuteScript(
288 GetLoginUI()->GetWebContents(),
289 "$('gaia-signin').gaiaAuthHost_.addEventListener('authFlowChange',"
291 "window.domAutomationController.setAutomationId(0);"
292 "window.domAutomationController.send("
293 "$('gaia-signin').isSAML() ? 'SamlLoaded' : 'GaiaLoaded');"
297 content::DOMMessageQueue message_queue
; // Start observe before SAML.
298 GetLoginDisplay()->ShowSigninScreenForCreds(gaia_email
, "");
301 ASSERT_TRUE(message_queue
.WaitForMessage(&message
));
302 EXPECT_EQ("\"SamlLoaded\"", message
);
305 void SetSignFormField(const std::string
& field_id
,
306 const std::string
& field_value
) {
309 "document.getElementById('$FieldId').value = '$FieldValue';"
310 "var e = new Event('input');"
311 "document.getElementById('$FieldId').dispatchEvent(e);"
313 ReplaceSubstringsAfterOffset(&js
, 0, "$FieldId", field_id
);
314 ReplaceSubstringsAfterOffset(&js
, 0, "$FieldValue", field_value
);
315 ExecuteJsInSigninFrame(js
);
318 void SendConfirmPassword(const std::string
& password_to_confirm
) {
320 "$('confirm-password-input').value='$Password';"
321 "$('confirm-password').onConfirmPassword_();";
322 ReplaceSubstringsAfterOffset(&js
, 0, "$Password", password_to_confirm
);
323 ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(), js
));
326 void JsExpect(const std::string
& js
) {
328 EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
329 GetLoginUI()->GetWebContents(),
330 "window.domAutomationController.send(!!(" + js
+ "));",
332 EXPECT_TRUE(result
) << js
;
335 content::WebUI
* GetLoginUI() {
336 return static_cast<chromeos::LoginDisplayHostImpl
*>(
337 chromeos::LoginDisplayHostImpl::default_host())->GetOobeUI()->web_ui();
340 // Executes Js code in the auth iframe hosted by gaia_auth extension.
341 void ExecuteJsInSigninFrame(const std::string
& js
) {
342 ASSERT_TRUE(content::ExecuteScriptInFrame(
343 GetLoginUI()->GetWebContents(),
344 "//iframe[@id='signin-frame']\n//iframe",
348 FakeSamlIdp
* fake_saml_idp() { return &fake_saml_idp_
; }
353 FakeSamlIdp fake_saml_idp_
;
355 bool saml_load_injected_
;
357 DISALLOW_COPY_AND_ASSIGN(SamlTest
);
360 // Tests that signin frame should have 'saml' class and 'cancel' button is
361 // visible when SAML IdP page is loaded. And 'cancel' button goes back to
363 IN_PROC_BROWSER_TEST_F(SamlTest
, SamlUI
) {
364 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
365 StartSamlAndWaitForIdpPageLoad(kUserEmail
);
367 // Saml flow UI expectations.
368 JsExpect("$('gaia-signin').classList.contains('saml')");
369 JsExpect("!$('cancel-add-user-button').hidden");
371 // Click on 'cancel'.
372 content::DOMMessageQueue message_queue
; // Observe before 'cancel'.
373 ASSERT_TRUE(content::ExecuteScript(
374 GetLoginUI()->GetWebContents(),
375 "$('cancel-add-user-button').click();"));
377 // Auth flow should change back to Gaia.
380 ASSERT_TRUE(message_queue
.WaitForMessage(&message
));
381 } while (message
!= "\"GaiaLoaded\"");
383 // Saml flow is gone.
384 JsExpect("!$('gaia-signin').classList.contains('saml')");
387 // Tests the sign-in flow when the credentials passing API is used.
388 IN_PROC_BROWSER_TEST_F(SamlTest
, CredentialPassingAPI
) {
389 fake_saml_idp()->SetLoginHTMLTemplate("saml_api_login.html");
390 fake_saml_idp()->SetLoginAuthHTMLTemplate("saml_api_login_auth.html");
391 StartSamlAndWaitForIdpPageLoad(kUserEmail
);
393 // Fill-in the SAML IdP form and submit.
394 SetSignFormField("Email", "fake_user");
395 SetSignFormField("Password", "fake_password");
396 ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
398 // Login should finish login and a session should start.
399 content::WindowedNotificationObserver(
400 chrome::NOTIFICATION_SESSION_STARTED
,
401 content::NotificationService::AllSources()).Wait();
404 // Tests the single password scraped flow.
405 IN_PROC_BROWSER_TEST_F(SamlTest
, ScrapedSingle
) {
406 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
407 StartSamlAndWaitForIdpPageLoad(kUserEmail
);
409 // Fill-in the SAML IdP form and submit.
410 SetSignFormField("Email", "fake_user");
411 SetSignFormField("Password", "fake_password");
412 ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
414 // Lands on confirm password screen.
415 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD
).Wait();
417 // Enter an unknown password should go back to confirm password screen.
418 SendConfirmPassword("wrong_password");
419 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD
).Wait();
421 // Enter a known password should finish login and start session.
422 SendConfirmPassword("fake_password");
423 content::WindowedNotificationObserver(
424 chrome::NOTIFICATION_SESSION_STARTED
,
425 content::NotificationService::AllSources()).Wait();
428 // Tests the multiple password scraped flow.
429 IN_PROC_BROWSER_TEST_F(SamlTest
, ScrapedMultiple
) {
430 fake_saml_idp()->SetLoginHTMLTemplate("saml_login_two_passwords.html");
432 StartSamlAndWaitForIdpPageLoad(kUserEmail
);
434 SetSignFormField("Email", "fake_user");
435 SetSignFormField("Password", "fake_password");
436 SetSignFormField("Password1", "password1");
437 ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
439 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD
).Wait();
441 // Either scraped password should be able to sign-in.
442 SendConfirmPassword("password1");
443 content::WindowedNotificationObserver(
444 chrome::NOTIFICATION_SESSION_STARTED
,
445 content::NotificationService::AllSources()).Wait();
448 // Tests the no password scraped flow.
449 IN_PROC_BROWSER_TEST_F(SamlTest
, ScrapedNone
) {
450 fake_saml_idp()->SetLoginHTMLTemplate("saml_login_no_passwords.html");
452 StartSamlAndWaitForIdpPageLoad(kUserEmail
);
454 SetSignFormField("Email", "fake_user");
455 ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
457 OobeScreenWaiter(OobeDisplay::SCREEN_MESSAGE_BOX
).Wait();
459 "$('message-box-title').textContent == "
460 "loadTimeData.getString('noPasswordWarningTitle')");
463 // Types |alice@example.com| into the GAIA login form but then authenticates as
464 // |bob@example.com| via SAML. Verifies that the logged-in user is correctly
465 // identified as Bob.
466 IN_PROC_BROWSER_TEST_F(SamlTest
, UseAutenticatedUserEmailAddress
) {
467 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
468 // Type |alice@example.com| into the GAIA login form.
469 StartSamlAndWaitForIdpPageLoad(kAnotherUserEmail
);
471 // Authenticate as bob@example.com via SAML (the |Email| provided here is
472 // irrelevant - the authenticated user's e-mail address that FakeGAIA
473 // reports was set via SetMergeSessionParams()).
474 SetSignFormField("Email", "fake_user");
475 SetSignFormField("Password", "fake_password");
476 ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
478 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD
).Wait();
480 SendConfirmPassword("fake_password");
481 content::WindowedNotificationObserver(
482 chrome::NOTIFICATION_SESSION_STARTED
,
483 content::NotificationService::AllSources()).Wait();
484 const User
* user
= UserManager::Get()->GetActiveUser();
486 EXPECT_EQ(kUserEmail
, user
->email());
490 } // namespace chromeos