OLD | NEW |
| (Empty) |
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. | |
4 | |
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" | |
36 | |
37 using net::test_server::BasicHttpResponse; | |
38 using net::test_server::HttpRequest; | |
39 using net::test_server::HttpResponse; | |
40 | |
41 namespace chromeos { | |
42 | |
43 namespace { | |
44 | |
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"; | |
53 | |
54 const char kAnotherUserEmail[] = "alice@example.com"; | |
55 const char kUserEmail[] = "bob@example.com"; | |
56 | |
57 const char kRelayState[] = "RelayState"; | |
58 | |
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. | |
63 class FakeSamlIdp { | |
64 public: | |
65 FakeSamlIdp(); | |
66 ~FakeSamlIdp(); | |
67 | |
68 void SetUp(const std::string& base_path, const GURL& gaia_url); | |
69 | |
70 void SetLoginHTMLTemplate(const std::string& template_file); | |
71 void SetLoginAuthHTMLTemplate(const std::string& template_file); | |
72 | |
73 scoped_ptr<HttpResponse> HandleRequest(const HttpRequest& request); | |
74 | |
75 private: | |
76 scoped_ptr<HttpResponse> BuildHTMLResponse(const std::string& html_template, | |
77 const std::string& relay_state, | |
78 const std::string& next_path); | |
79 | |
80 base::FilePath html_template_dir_; | |
81 | |
82 std::string login_path_; | |
83 std::string login_auth_path_; | |
84 | |
85 std::string login_html_template_; | |
86 std::string login_auth_html_template_; | |
87 GURL gaia_assertion_url_; | |
88 | |
89 DISALLOW_COPY_AND_ASSIGN(FakeSamlIdp); | |
90 }; | |
91 | |
92 FakeSamlIdp::FakeSamlIdp() { | |
93 } | |
94 | |
95 FakeSamlIdp::~FakeSamlIdp() { | |
96 } | |
97 | |
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"); | |
102 | |
103 login_path_= base_path; | |
104 login_auth_path_ = base_path + "Auth"; | |
105 gaia_assertion_url_ = gaia_url.Resolve("/SSO"); | |
106 } | |
107 | |
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_)); | |
112 } | |
113 | |
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_)); | |
118 } | |
119 | |
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(); | |
126 | |
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_, | |
131 relay_state, | |
132 login_auth_path_); | |
133 } | |
134 | |
135 if (request_path != login_auth_path_) { | |
136 // Request not understood. | |
137 return scoped_ptr<HttpResponse>(); | |
138 } | |
139 | |
140 std::string relay_state; | |
141 FakeGaia::GetQueryParameter(request.content, kRelayState, &relay_state); | |
142 GURL redirect_url = gaia_assertion_url_; | |
143 | |
144 if (!login_auth_html_template_.empty()) { | |
145 return BuildHTMLResponse(login_auth_html_template_, | |
146 relay_state, | |
147 redirect_url.spec()); | |
148 } | |
149 | |
150 redirect_url = net::AppendQueryParameter( | |
151 redirect_url, "SAMLResponse", "fake_response"); | |
152 redirect_url = net::AppendQueryParameter( | |
153 redirect_url, kRelayState, relay_state); | |
154 | |
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>(); | |
159 } | |
160 | |
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); | |
168 | |
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"); | |
173 | |
174 return http_response.PassAs<HttpResponse>(); | |
175 } | |
176 | |
177 } // namespace | |
178 | |
179 class SamlTest : public InProcessBrowserTest { | |
180 public: | |
181 SamlTest() : saml_load_injected_(false) {} | |
182 virtual ~SamlTest() {} | |
183 | |
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()); | |
188 | |
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(); | |
192 | |
193 InProcessBrowserTest::SetUp(); | |
194 } | |
195 | |
196 virtual void SetUpInProcessBrowserTestFixture() OVERRIDE { | |
197 host_resolver()->AddRule("*", "127.0.0.1"); | |
198 } | |
199 | |
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 | |
206 const GURL& server_url = embedded_test_server()->base_url(); | |
207 | |
208 std::string gaia_host("gaia"); | |
209 GURL::Replacements replace_gaia_host; | |
210 replace_gaia_host.SetHostStr(gaia_host); | |
211 gaia_url_ = server_url.ReplaceComponents(replace_gaia_host); | |
212 | |
213 command_line->AppendSwitchASCII(::switches::kGaiaUrl, gaia_url_.spec()); | |
214 command_line->AppendSwitchASCII(::switches::kLsoUrl, gaia_url_.spec()); | |
215 command_line->AppendSwitchASCII(::switches::kGoogleApisUrl, | |
216 gaia_url_.spec()); | |
217 fake_gaia_.Initialize(); | |
218 | |
219 std::string saml_idp_host("saml.idp"); | |
220 GURL::Replacements replace_saml_idp_host; | |
221 replace_saml_idp_host.SetHostStr(saml_idp_host); | |
222 GURL saml_idp_url = server_url.ReplaceComponents(replace_saml_idp_host); | |
223 saml_idp_url = saml_idp_url.Resolve("/SAML/SSO"); | |
224 | |
225 fake_saml_idp_.SetUp(saml_idp_url.path(), gaia_url_); | |
226 fake_gaia_.RegisterSamlUser(kAnotherUserEmail, saml_idp_url); | |
227 fake_gaia_.RegisterSamlUser(kUserEmail, saml_idp_url); | |
228 } | |
229 | |
230 virtual void SetUpOnMainThread() OVERRIDE { | |
231 FakeGaia::MergeSessionParams params; | |
232 params.auth_sid_cookie = kTestAuthSIDCookie; | |
233 params.auth_lsid_cookie = kTestAuthLSIDCookie; | |
234 params.auth_code = kTestAuthCode; | |
235 params.refresh_token = kTestRefreshToken; | |
236 params.access_token = kTestAuthLoginAccessToken; | |
237 params.gaia_uber_token = kTestGaiaUberToken; | |
238 params.session_sid_cookie = kTestSessionSIDCookie; | |
239 params.session_lsid_cookie = kTestSessionLSIDCookie; | |
240 params.email = kUserEmail; | |
241 fake_gaia_.SetMergeSessionParams(params); | |
242 | |
243 embedded_test_server()->RegisterRequestHandler( | |
244 base::Bind(&FakeGaia::HandleRequest, base::Unretained(&fake_gaia_))); | |
245 embedded_test_server()->RegisterRequestHandler(base::Bind( | |
246 &FakeSamlIdp::HandleRequest, base::Unretained(&fake_saml_idp_))); | |
247 | |
248 // Restart the thread as the sandbox host process has already been spawned. | |
249 embedded_test_server()->RestartThreadAndListen(); | |
250 } | |
251 | |
252 virtual void CleanUpOnMainThread() OVERRIDE { | |
253 // If the login display is still showing, exit gracefully. | |
254 if (LoginDisplayHostImpl::default_host()) { | |
255 base::MessageLoop::current()->PostTask(FROM_HERE, | |
256 base::Bind(&chrome::AttemptExit)); | |
257 content::RunMessageLoop(); | |
258 } | |
259 } | |
260 | |
261 WebUILoginDisplay* GetLoginDisplay() { | |
262 ExistingUserController* controller = | |
263 ExistingUserController::current_controller(); | |
264 CHECK(controller); | |
265 return static_cast<WebUILoginDisplay*>(controller->login_display()); | |
266 } | |
267 | |
268 void WaitForSigninScreen() { | |
269 WizardController::SkipPostLoginScreensForTesting(); | |
270 WizardController* wizard_controller = | |
271 chromeos::WizardController::default_controller(); | |
272 CHECK(wizard_controller); | |
273 wizard_controller->SkipToLoginForTesting(LoginScreenContext()); | |
274 | |
275 content::WindowedNotificationObserver( | |
276 chrome::NOTIFICATION_LOGIN_OR_LOCK_WEBUI_VISIBLE, | |
277 content::NotificationService::AllSources()).Wait(); | |
278 } | |
279 | |
280 void StartSamlAndWaitForIdpPageLoad(const std::string& gaia_email) { | |
281 WaitForSigninScreen(); | |
282 | |
283 if (!saml_load_injected_) { | |
284 saml_load_injected_ = true; | |
285 | |
286 ASSERT_TRUE(content::ExecuteScript( | |
287 GetLoginUI()->GetWebContents(), | |
288 "$('gaia-signin').gaiaAuthHost_.addEventListener('authFlowChange'," | |
289 "function() {" | |
290 "window.domAutomationController.setAutomationId(0);" | |
291 "window.domAutomationController.send(" | |
292 "$('gaia-signin').isSAML() ? 'SamlLoaded' : 'GaiaLoaded');" | |
293 "});")); | |
294 } | |
295 | |
296 content::DOMMessageQueue message_queue; // Start observe before SAML. | |
297 GetLoginDisplay()->ShowSigninScreenForCreds(gaia_email, ""); | |
298 | |
299 std::string message; | |
300 ASSERT_TRUE(message_queue.WaitForMessage(&message)); | |
301 EXPECT_EQ("\"SamlLoaded\"", message); | |
302 } | |
303 | |
304 void SetSignFormField(const std::string& field_id, | |
305 const std::string& field_value) { | |
306 std::string js = | |
307 "(function(){" | |
308 "document.getElementById('$FieldId').value = '$FieldValue';" | |
309 "var e = new Event('input');" | |
310 "document.getElementById('$FieldId').dispatchEvent(e);" | |
311 "})();"; | |
312 ReplaceSubstringsAfterOffset(&js, 0, "$FieldId", field_id); | |
313 ReplaceSubstringsAfterOffset(&js, 0, "$FieldValue", field_value); | |
314 ExecuteJsInSigninFrame(js); | |
315 } | |
316 | |
317 void SendConfirmPassword(const std::string& password_to_confirm) { | |
318 std::string js = | |
319 "$('confirm-password-input').value='$Password';" | |
320 "$('confirm-password').onConfirmPassword_();"; | |
321 ReplaceSubstringsAfterOffset(&js, 0, "$Password", password_to_confirm); | |
322 ASSERT_TRUE(content::ExecuteScript(GetLoginUI()->GetWebContents(), js)); | |
323 } | |
324 | |
325 void JsExpect(const std::string& js) { | |
326 bool result; | |
327 EXPECT_TRUE(content::ExecuteScriptAndExtractBool( | |
328 GetLoginUI()->GetWebContents(), | |
329 "window.domAutomationController.send(!!(" + js + "));", | |
330 &result)); | |
331 EXPECT_TRUE(result) << js; | |
332 } | |
333 | |
334 content::WebUI* GetLoginUI() { | |
335 return static_cast<chromeos::LoginDisplayHostImpl*>( | |
336 chromeos::LoginDisplayHostImpl::default_host())->GetOobeUI()->web_ui(); | |
337 } | |
338 | |
339 // Executes Js code in the auth iframe hosted by gaia_auth extension. | |
340 void ExecuteJsInSigninFrame(const std::string& js) { | |
341 ASSERT_TRUE(content::ExecuteScriptInFrame( | |
342 GetLoginUI()->GetWebContents(), | |
343 "//iframe[@id='signin-frame']\n//iframe", | |
344 js)); | |
345 } | |
346 | |
347 FakeSamlIdp* fake_saml_idp() { return &fake_saml_idp_; } | |
348 | |
349 private: | |
350 GURL gaia_url_; | |
351 FakeGaia fake_gaia_; | |
352 FakeSamlIdp fake_saml_idp_; | |
353 | |
354 bool saml_load_injected_; | |
355 | |
356 DISALLOW_COPY_AND_ASSIGN(SamlTest); | |
357 }; | |
358 | |
359 // Tests that signin frame should have 'saml' class and 'cancel' button is | |
360 // visible when SAML IdP page is loaded. And 'cancel' button goes back to | |
361 // gaia on clicking. | |
362 IN_PROC_BROWSER_TEST_F(SamlTest, SamlUI) { | |
363 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html"); | |
364 StartSamlAndWaitForIdpPageLoad(kUserEmail); | |
365 | |
366 // Saml flow UI expectations. | |
367 JsExpect("$('gaia-signin').classList.contains('saml')"); | |
368 JsExpect("!$('cancel-add-user-button').hidden"); | |
369 | |
370 // Click on 'cancel'. | |
371 content::DOMMessageQueue message_queue; // Observe before 'cancel'. | |
372 ASSERT_TRUE(content::ExecuteScript( | |
373 GetLoginUI()->GetWebContents(), | |
374 "$('cancel-add-user-button').click();")); | |
375 | |
376 // Auth flow should change back to Gaia. | |
377 std::string message; | |
378 do { | |
379 ASSERT_TRUE(message_queue.WaitForMessage(&message)); | |
380 } while (message != "\"GaiaLoaded\""); | |
381 | |
382 // Saml flow is gone. | |
383 JsExpect("!$('gaia-signin').classList.contains('saml')"); | |
384 } | |
385 | |
386 // Tests the sign-in flow when the credentials passing API is used. | |
387 IN_PROC_BROWSER_TEST_F(SamlTest, CredentialPassingAPI) { | |
388 fake_saml_idp()->SetLoginHTMLTemplate("saml_api_login.html"); | |
389 fake_saml_idp()->SetLoginAuthHTMLTemplate("saml_api_login_auth.html"); | |
390 StartSamlAndWaitForIdpPageLoad(kUserEmail); | |
391 | |
392 // Fill-in the SAML IdP form and submit. | |
393 SetSignFormField("Email", "fake_user"); | |
394 SetSignFormField("Password", "fake_password"); | |
395 ExecuteJsInSigninFrame("document.getElementById('Submit').click();"); | |
396 | |
397 // Login should finish login and a session should start. | |
398 content::WindowedNotificationObserver( | |
399 chrome::NOTIFICATION_SESSION_STARTED, | |
400 content::NotificationService::AllSources()).Wait(); | |
401 } | |
402 | |
403 // Tests the single password scraped flow. | |
404 IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedSingle) { | |
405 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html"); | |
406 StartSamlAndWaitForIdpPageLoad(kUserEmail); | |
407 | |
408 // Fill-in the SAML IdP form and submit. | |
409 SetSignFormField("Email", "fake_user"); | |
410 SetSignFormField("Password", "fake_password"); | |
411 ExecuteJsInSigninFrame("document.getElementById('Submit').click();"); | |
412 | |
413 // Lands on confirm password screen. | |
414 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD).Wait(); | |
415 | |
416 // Enter an unknown password should go back to confirm password screen. | |
417 SendConfirmPassword("wrong_password"); | |
418 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD).Wait(); | |
419 | |
420 // Enter a known password should finish login and start session. | |
421 SendConfirmPassword("fake_password"); | |
422 content::WindowedNotificationObserver( | |
423 chrome::NOTIFICATION_SESSION_STARTED, | |
424 content::NotificationService::AllSources()).Wait(); | |
425 } | |
426 | |
427 // Tests the multiple password scraped flow. | |
428 IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedMultiple) { | |
429 fake_saml_idp()->SetLoginHTMLTemplate("saml_login_two_passwords.html"); | |
430 | |
431 StartSamlAndWaitForIdpPageLoad(kUserEmail); | |
432 | |
433 SetSignFormField("Email", "fake_user"); | |
434 SetSignFormField("Password", "fake_password"); | |
435 SetSignFormField("Password1", "password1"); | |
436 ExecuteJsInSigninFrame("document.getElementById('Submit').click();"); | |
437 | |
438 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD).Wait(); | |
439 | |
440 // Either scraped password should be able to sign-in. | |
441 SendConfirmPassword("password1"); | |
442 content::WindowedNotificationObserver( | |
443 chrome::NOTIFICATION_SESSION_STARTED, | |
444 content::NotificationService::AllSources()).Wait(); | |
445 } | |
446 | |
447 // Tests the no password scraped flow. | |
448 IN_PROC_BROWSER_TEST_F(SamlTest, ScrapedNone) { | |
449 fake_saml_idp()->SetLoginHTMLTemplate("saml_login_no_passwords.html"); | |
450 | |
451 StartSamlAndWaitForIdpPageLoad(kUserEmail); | |
452 | |
453 SetSignFormField("Email", "fake_user"); | |
454 ExecuteJsInSigninFrame("document.getElementById('Submit').click();"); | |
455 | |
456 OobeScreenWaiter(OobeDisplay::SCREEN_MESSAGE_BOX).Wait(); | |
457 JsExpect( | |
458 "$('message-box-title').textContent == " | |
459 "loadTimeData.getString('noPasswordWarningTitle')"); | |
460 } | |
461 | |
462 // Types |alice@example.com| into the GAIA login form but then authenticates as | |
463 // |bob@example.com| via SAML. Verifies that the logged-in user is correctly | |
464 // identified as Bob. | |
465 IN_PROC_BROWSER_TEST_F(SamlTest, UseAutenticatedUserEmailAddress) { | |
466 fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html"); | |
467 // Type |alice@example.com| into the GAIA login form. | |
468 StartSamlAndWaitForIdpPageLoad(kAnotherUserEmail); | |
469 | |
470 // Authenticate as bob@example.com via SAML (the |Email| provided here is | |
471 // irrelevant - the authenticated user's e-mail address that FakeGAIA | |
472 // reports was set via SetMergeSessionParams()). | |
473 SetSignFormField("Email", "fake_user"); | |
474 SetSignFormField("Password", "fake_password"); | |
475 ExecuteJsInSigninFrame("document.getElementById('Submit').click();"); | |
476 | |
477 OobeScreenWaiter(OobeDisplay::SCREEN_CONFIRM_PASSWORD).Wait(); | |
478 | |
479 SendConfirmPassword("fake_password"); | |
480 content::WindowedNotificationObserver( | |
481 chrome::NOTIFICATION_SESSION_STARTED, | |
482 content::NotificationService::AllSources()).Wait(); | |
483 const User* user = UserManager::Get()->GetActiveUser(); | |
484 ASSERT_TRUE(user); | |
485 EXPECT_EQ(kUserEmail, user->email()); | |
486 } | |
487 | |
488 | |
489 } // namespace chromeos | |
OLD | NEW |