blob: b1b3026bae4ba4f3bfebbbf7411cd7750184fac8 [file] [log] [blame]
// Copyright 2016 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 "base/command_line.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/password_manager/chrome_password_manager_client.h"
#include "chrome/browser/password_manager/password_manager_test_base.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_io_data.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/passwords/passwords_model_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/password_manager/core/browser/password_bubble_experiment.h"
#include "components/password_manager/core/browser/test_password_store.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
namespace {
class CredentialManagerBrowserTest : public PasswordManagerBrowserTestBase {
public:
CredentialManagerBrowserTest() {
scoped_feature_list_.InitAndEnableFeature(features::kWebAuth);
}
void SetUpOnMainThread() override {
PasswordManagerBrowserTestBase::SetUpOnMainThread();
// Redirect all requests to localhost.
host_resolver()->AddRule("*", "127.0.0.1");
}
bool IsShowingAccountChooser() {
return PasswordsModelDelegateFromWebContents(WebContents())->GetState() ==
password_manager::ui::CREDENTIAL_REQUEST_STATE;
}
void SetUpCommandLine(base::CommandLine* command_line) override {
// To permit using webauthentication features.
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
// Similarly to PasswordManagerBrowserTestBase::NavigateToFile this is a
// wrapper around ui_test_utils::NavigateURL that waits until DidFinishLoad()
// fires. Different to NavigateToFile this method allows passing a test_server
// and modifications to the hostname.
void NavigateToURL(const net::EmbeddedTestServer& test_server,
const std::string& hostname,
const std::string& relative_url) {
NavigationObserver observer(WebContents());
GURL url = test_server.GetURL(hostname, relative_url);
ui_test_utils::NavigateToURL(browser(), url);
observer.Wait();
}
// Triggers a call to `navigator.credentials.get` to retrieve passwords, waits
// for success, and ASSERTs that |expect_has_results| is satisfied.
void TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
content::WebContents* web_contents,
bool expect_has_results) {
bool result = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
web_contents,
"navigator.credentials.get({password: true}).then(c => {"
" window.domAutomationController.send(!!c);"
"});",
&result));
ASSERT_EQ(expect_has_results, result);
}
// Attempt to create a publicKeyCredential with an unsupported algorithm type.
void CreatePublicKeyCredentialWithUnsupportedAlgorithmAndExpectNotSupported(
content::WebContents* web_contents) {
std::string result;
std::string script =
"navigator.credentials.create({ publicKey: {"
" challenge: new TextEncoder().encode('climb a mountain'),"
" rp: { id: '1098237235409872', name: 'Acme' },"
" user: { "
" id: new TextEncoder().encode('1098237235409872'),"
" name: 'avery.a.jones@example.com',"
" displayName: 'Avery A. Jones', "
" icon: 'https://pics.acme.com/00/p/aBjjjpqPb.png'},"
" pubKeyCredParams: [{ type: 'public-key', alg: '123'}],"
" timeout: 60000,"
" excludeList: [] }"
"}).catch(c => window.domAutomationController.send(c.toString()));";
ASSERT_TRUE(
content::ExecuteScriptAndExtractString(web_contents, script, &result));
ASSERT_EQ(
"NotSupportedError: Parameters for this operation are not supported.",
result);
}
// Schedules a call to be made to navigator.credentials.store() in the
// `unload` handler to save a credential with |username| and |password|.
void ScheduleNavigatorStoreCredentialAtUnload(
content::WebContents* web_contents,
const char* username,
const char* password) {
ASSERT_TRUE(content::ExecuteScript(
web_contents,
base::StringPrintf(
"window.addEventListener(\"unload\", () => {"
" var c = new PasswordCredential({ id: '%s', password: '%s' });"
" navigator.credentials.store(c);"
"});",
username, password)));
}
// Tests that when navigator.credentials.store() is called in an `unload`
// handler before a same-RenderFrame navigation, the request is either dropped
// or serviced in the context of the old document.
//
// If |preestablish_mojo_pipe| is set, then the CredentialManagerClient will
// establish the Mojo connection to the ContentCredentialManager ahead of
// time, instead of letting the Mojo connection be established on-demand when
// the call to store() triggered from the unload handler.
void TestStoreInUnloadHandlerForSameSiteNavigation(
bool preestablish_mojo_pipe) {
// Use URLs that differ on subdomains so we can tell which one was used for
// saving, but they still belong to the same SiteInstance, so they will be
// renderered in the same RenderFrame (in the same process).
const GURL a_url1 = https_test_server().GetURL("foo.a.com", "/title1.html");
const GURL a_url2 = https_test_server().GetURL("bar.a.com", "/title2.html");
// Navigate to a mostly empty page.
ui_test_utils::NavigateToURL(browser(), a_url1);
ChromePasswordManagerClient* client =
ChromePasswordManagerClient::FromWebContents(WebContents());
EXPECT_FALSE(client->has_binding_for_credential_manager());
if (preestablish_mojo_pipe) {
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
WebContents(), false));
EXPECT_TRUE(client->has_binding_for_credential_manager());
}
// Schedule storing a credential on the `unload` event.
ASSERT_NO_FATAL_FAILURE(ScheduleNavigatorStoreCredentialAtUnload(
WebContents(), "user", "hunter2"));
// Trigger a same-site navigation carried out in the same RenderFrame.
content::RenderFrameHost* old_rfh = WebContents()->GetMainFrame();
ui_test_utils::NavigateToURL(browser(), a_url2);
ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
// Ensure that the old document no longer has a mojom::CredentialManager
// interface connection to the ContentCredentialManager, nor can it get one
// later.
//
// The sequence of events for same-RFH navigations is as follows:
// 1.) FrameHostMsg_DidStartProvisionalLoad
// ... waiting for first response byte ...
// 2.) FrameLoader::PrepareForCommit
// 2.1) Document::Shutdown (old Document)
// 3.) mojom::FrameHost::DidCommitProvisionalLoad (new load)
// ... loading ...
// 4.) FrameHostMsg_DidStopLoading
// 5.) content::WaitForLoadStop inside NavigateToURL returns
// 6.) NavigateToURL returns
//
// After Step 2.1, the old Document no longer executes any author JS, so
// there can be no more calls to the Credential Management API, hence no
// more InterfaceRequests for mojom::CredentialManager.
//
// Because the InterfaceRegistry, through which the client end of the
// mojom::CredentialManager interface to the ContentCredentialManager is
// retrieved, is re-bound by the RenderFrameHostImpl to a new pipe on
// DidCommitProvisionalLoad, any InterfaceRequest messages issued before or
// during Step 2.1 will either have already been dispatched on the browser
// side and serviced before DidCommitProvisionalLoad in Step 3, or will be
// ignored altogether.
//
// Hence it is sufficient to check that the Mojo connection is closed now
// after NavigateToURL above has returned.
EXPECT_FALSE(client->has_binding_for_credential_manager());
// Ensure that the navigator.credentials.store() call issued on the previous
// mojom::CredentialManager connection was either serviced in the context of
// the old URL, |a_url|, or dropped altogether.
//
// The behavior is non-deterministic because the mojom::CredentialManager
// interface is not Channel-associated, so message ordering with legacy IPC
// messages is not preserved.
//
// If the store() method invoked from the `unload` handler (in Step 2.1)
// happens to be speedily dispatched before DidCommitProvisionalLoad, it
// will have been serviced in the context of the old document. Otherwise the
// ContentCredentialManager should have closed the underlying interface
// connection in response to DidCommitProvisionalLoad in Step 3, and the
// method call should be ignored.
if (!client->was_store_ever_called())
return;
BubbleObserver prompt_observer(WebContents());
prompt_observer.WaitForAutomaticSavePrompt();
ASSERT_TRUE(prompt_observer.IsSavePromptShownAutomatically());
prompt_observer.AcceptSavePrompt();
WaitForPasswordStore();
password_manager::TestPasswordStore* test_password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
ASSERT_EQ(1u, test_password_store->stored_passwords().size());
autofill::PasswordForm signin_form =
test_password_store->stored_passwords().begin()->second[0];
EXPECT_EQ(base::ASCIIToUTF16("user"), signin_form.username_value);
EXPECT_EQ(base::ASCIIToUTF16("hunter2"), signin_form.password_value);
EXPECT_EQ(a_url1.GetOrigin(), signin_form.origin);
}
// Tests the when navigator.credentials.store() is called in an `unload`
// handler before a cross-site transfer navigation, the request is ignored.
//
// If |preestablish_mojo_pipe| is set, then the CredentialManagerClient will
// establish the Mojo connection to the ContentCredentialManager ahead of
// time, instead of letting the Mojo connection be established on-demand when
// the call to store() triggered from the unload handler.
void TestStoreInUnloadHandlerForCrossSiteNavigation(
bool preestablish_mojo_pipe) {
const GURL a_url = https_test_server().GetURL("a.com", "/title1.html");
const GURL b_url = https_test_server().GetURL("b.com", "/title2.html");
// Navigate to a mostly empty page.
ui_test_utils::NavigateToURL(browser(), a_url);
ChromePasswordManagerClient* client =
ChromePasswordManagerClient::FromWebContents(WebContents());
if (preestablish_mojo_pipe) {
EXPECT_FALSE(client->has_binding_for_credential_manager());
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(
WebContents(), false));
EXPECT_TRUE(client->has_binding_for_credential_manager());
}
// Schedule storing a credential on the `unload` event.
ASSERT_NO_FATAL_FAILURE(ScheduleNavigatorStoreCredentialAtUnload(
WebContents(), "user", "hunter2"));
// Trigger a cross-site navigation that is carried out in a new renderer,
// and which will swap out the old RenderFrameHost.
content::RenderFrameDeletedObserver rfh_destruction_observer(
WebContents()->GetMainFrame());
ui_test_utils::NavigateToURL(browser(), b_url);
// Ensure that the navigator.credentials.store() call is never serviced.
// The sufficient conditions for this are:
// -- The swapped out RFH is destroyed, so the RenderFrame cannot
// establish a new Mojo connection to ContentCredentialManager anymore.
// -- There is no pre-existing Mojo connection to ContentCredentialManager
// either, which could be used to call store() in the future.
// -- There have not been any calls to store() in the past.
rfh_destruction_observer.WaitUntilDeleted();
EXPECT_FALSE(client->has_binding_for_credential_manager());
EXPECT_FALSE(client->was_store_ever_called());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
DISALLOW_COPY_AND_ASSIGN(CredentialManagerBrowserTest);
};
// Tests.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
AccountChooserWithOldCredentialAndNavigation) {
// Save credentials with 'skip_zero_click'.
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
autofill::PasswordForm signin_form;
signin_form.signon_realm = embedded_test_server()->base_url().spec();
signin_form.password_value = base::ASCIIToUTF16("password");
signin_form.username_value = base::ASCIIToUTF16("user");
signin_form.origin = embedded_test_server()->base_url();
signin_form.skip_zero_click = true;
password_store->AddLogin(signin_form);
NavigateToFile("/password/password_form.html");
std::string fill_password =
"document.getElementById('username_field').value = 'user';"
"document.getElementById('password_field').value = 'password';";
ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_password));
// Call the API to trigger the notification to the client.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.get({password: true})"
".then(cred => window.location = '/password/done.html')"));
// Mojo calls from the renderer are asynchronous.
BubbleObserver(WebContents()).WaitForAccountChooser();
PasswordsModelDelegateFromWebContents(WebContents())
->ChooseCredential(
signin_form,
password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD);
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
// Verify that the form's 'skip_zero_click' is updated and not overwritten
// by the autofill password manager on successful login.
WaitForPasswordStore();
password_manager::TestPasswordStore::PasswordMap passwords_map =
password_store->stored_passwords();
ASSERT_EQ(1u, passwords_map.size());
const std::vector<autofill::PasswordForm>& passwords_vector =
passwords_map.begin()->second;
ASSERT_EQ(1u, passwords_vector.size());
const autofill::PasswordForm& form = passwords_vector[0];
EXPECT_EQ(base::ASCIIToUTF16("user"), form.username_value);
EXPECT_EQ(base::ASCIIToUTF16("password"), form.password_value);
EXPECT_FALSE(form.skip_zero_click);
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreExistingCredentialIsNoOp) {
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
GURL origin = embedded_test_server()->base_url();
autofill::PasswordForm form_1;
form_1.signon_realm = origin.spec();
form_1.username_value = base::ASCIIToUTF16("user1");
form_1.password_value = base::ASCIIToUTF16("abcdef");
form_1.preferred = true;
autofill::PasswordForm form_2;
form_2.signon_realm = origin.spec();
form_2.username_value = base::ASCIIToUTF16("user2");
form_2.password_value = base::ASCIIToUTF16("123456");
password_store->AddLogin(form_1);
password_store->AddLogin(form_2);
WaitForPasswordStore();
// Check that the password store contains the values we expect.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("abcdef"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("123456"), passwords[1].password_value);
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user1' with the old password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user1', password: 'abcdef' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user2' with the old password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user2', password: '123456' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
// Wait for the password store to process the store request.
WaitForPasswordStore();
// Check that the password still store contains the values we expect.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("abcdef"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("123456"), passwords[1].password_value);
}
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreUpdatesPasswordOfExistingCredential) {
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
GURL origin = embedded_test_server()->base_url();
autofill::PasswordForm form_1;
form_1.signon_realm = origin.spec();
form_1.username_value = base::ASCIIToUTF16("user1");
form_1.password_value = base::ASCIIToUTF16("abcdef");
form_1.preferred = true;
autofill::PasswordForm form_2;
form_2.signon_realm = origin.spec();
form_2.username_value = base::ASCIIToUTF16("user2");
form_2.password_value = base::ASCIIToUTF16("123456");
password_store->AddLogin(form_1);
password_store->AddLogin(form_2);
WaitForPasswordStore();
// Check that the password store contains the values we expect.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("abcdef"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("123456"), passwords[1].password_value);
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user1' with a new password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user1', password: 'ABCDEF' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user2' with a new password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user2', password: 'UVWXYZ' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
// Wait for the password store to process the store request.
WaitForPasswordStore();
// Check that the password store contains the values we expect.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("ABCDEF"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("UVWXYZ"), passwords[1].password_value);
}
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreUpdatesPasswordOfExistingCredentialWithAttributes) {
// This test is the same as the previous one, except that the already existing
// credentials contain metadata.
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
GURL origin = embedded_test_server()->base_url();
autofill::PasswordForm form_1;
form_1.signon_realm = origin.spec();
form_1.username_value = base::ASCIIToUTF16("user1");
form_1.password_value = base::ASCIIToUTF16("abcdef");
form_1.username_element = base::ASCIIToUTF16("user");
form_1.password_element = base::ASCIIToUTF16("pass");
form_1.origin = GURL(origin.spec() + "/my/custom/path/");
form_1.preferred = true;
autofill::PasswordForm form_2;
form_2.signon_realm = origin.spec();
form_2.username_value = base::ASCIIToUTF16("user2");
form_2.password_value = base::ASCIIToUTF16("123456");
form_2.username_element = base::ASCIIToUTF16("username");
form_2.password_element = base::ASCIIToUTF16("password");
form_2.origin = GURL(origin.spec() + "/my/other/path/");
password_store->AddLogin(form_1);
password_store->AddLogin(form_2);
WaitForPasswordStore();
// Check that the password store contains the values we expect.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("abcdef"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user"), passwords[0].username_element);
EXPECT_EQ(base::ASCIIToUTF16("pass"), passwords[0].password_element);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("123456"), passwords[1].password_value);
EXPECT_EQ(base::ASCIIToUTF16("username"), passwords[1].username_element);
EXPECT_EQ(base::ASCIIToUTF16("password"), passwords[1].password_element);
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user1' with a new password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user1', password: 'ABCDEF' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
{
NavigateToFile("/password/simple_password.html");
// Call the API to store 'user2' with a new password.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user2', password: 'UVWXYZ' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
}
// Wait for the password store to process the store request.
WaitForPasswordStore();
// Check that the password store contains the values we expect.
// Note that we don't check for username and password elements, as they don't
// exist for credentials saved by the API.
{
auto found = password_store->stored_passwords().find(origin.spec());
ASSERT_NE(password_store->stored_passwords().end(), found);
const std::vector<autofill::PasswordForm>& passwords = found->second;
ASSERT_EQ(2U, passwords.size());
EXPECT_EQ(base::ASCIIToUTF16("user1"), passwords[0].username_value);
EXPECT_EQ(base::ASCIIToUTF16("ABCDEF"), passwords[0].password_value);
EXPECT_EQ(base::ASCIIToUTF16("user2"), passwords[1].username_value);
EXPECT_EQ(base::ASCIIToUTF16("UVWXYZ"), passwords[1].password_value);
}
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreSavesPSLMatchedCredential) {
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
// The call to |GetURL| is needed to get the correct port.
GURL psl_url = https_test_server().GetURL("psl.example.com", "/");
autofill::PasswordForm signin_form;
signin_form.signon_realm = psl_url.spec();
signin_form.password_value = base::ASCIIToUTF16("password");
signin_form.username_value = base::ASCIIToUTF16("user");
signin_form.origin = psl_url;
password_store->AddLogin(signin_form);
NavigateToURL(https_test_server(), "www.example.com",
"/password/password_form.html");
// Call the API to trigger |get| and |store| and redirect.
ASSERT_TRUE(
content::ExecuteScript(RenderViewHost(),
"navigator.credentials.get({password: true})"
".then(cred => "
"navigator.credentials.store(cred)"
".then(cred => "
"window.location = '/password/done.html'))"));
// Mojo calls from the renderer are asynchronous.
BubbleObserver(WebContents()).WaitForAccountChooser();
PasswordsModelDelegateFromWebContents(WebContents())
->ChooseCredential(
signin_form,
password_manager::CredentialType::CREDENTIAL_TYPE_PASSWORD);
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
// Wait for the password store before checking the prompt because it pops up
// after the store replies.
WaitForPasswordStore();
BubbleObserver prompt_observer(WebContents());
EXPECT_FALSE(prompt_observer.IsSavePromptShownAutomatically());
EXPECT_FALSE(prompt_observer.IsUpdatePromptShownAutomatically());
// There should be an entry for both psl.example.com and www.example.com.
password_manager::TestPasswordStore::PasswordMap passwords =
password_store->stored_passwords();
GURL www_url = https_test_server().GetURL("www.example.com", "/");
EXPECT_EQ(2U, passwords.size());
EXPECT_TRUE(base::ContainsKey(passwords, psl_url.spec()));
EXPECT_TRUE(base::ContainsKey(passwords, www_url.spec()));
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
UpdatingPSLMatchedCredentialCreatesSecondEntry) {
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
// The call to |GetURL| is needed to get the correct port.
GURL psl_url = https_test_server().GetURL("psl.example.com", "/");
autofill::PasswordForm signin_form;
signin_form.signon_realm = psl_url.spec();
signin_form.password_value = base::ASCIIToUTF16("password");
signin_form.username_value = base::ASCIIToUTF16("user");
signin_form.origin = psl_url;
password_store->AddLogin(signin_form);
NavigateToURL(https_test_server(), "www.example.com",
"/password/password_form.html");
// Call the API to trigger |get| and |store| and redirect.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.store("
" new PasswordCredential({ id: 'user', password: 'P4SSW0RD' }))"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
BubbleObserver prompt_observer(WebContents());
prompt_observer.WaitForAutomaticSavePrompt();
ASSERT_TRUE(prompt_observer.IsSavePromptShownAutomatically());
prompt_observer.AcceptSavePrompt();
WaitForPasswordStore();
// There should be an entry for both psl.example.com and www.example.com.
password_manager::TestPasswordStore::PasswordMap passwords =
password_store->stored_passwords();
GURL www_url = https_test_server().GetURL("www.example.com", "/");
EXPECT_EQ(2U, passwords.size());
EXPECT_TRUE(base::ContainsKey(passwords, psl_url.spec()));
EXPECT_TRUE(base::ContainsKey(passwords, www_url.spec()));
EXPECT_EQ(base::ASCIIToUTF16("user"),
passwords[psl_url.spec()].front().username_value);
EXPECT_EQ(base::ASCIIToUTF16("password"),
passwords[psl_url.spec()].front().password_value);
EXPECT_EQ(base::ASCIIToUTF16("user"),
passwords[www_url.spec()].front().username_value);
EXPECT_EQ(base::ASCIIToUTF16("P4SSW0RD"),
passwords[www_url.spec()].front().password_value);
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
ObsoleteHttpCredentialMovedOnMigrationToHstsSite) {
// Add an http credential to the password store.
GURL https_origin = https_test_server().base_url();
ASSERT_TRUE(https_origin.SchemeIs(url::kHttpsScheme));
GURL::Replacements rep;
rep.SetSchemeStr(url::kHttpScheme);
GURL http_origin = https_origin.ReplaceComponents(rep);
autofill::PasswordForm http_form;
http_form.signon_realm = http_origin.spec();
http_form.origin = http_origin;
http_form.username_value = base::ASCIIToUTF16("user");
http_form.password_value = base::ASCIIToUTF16("12345");
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
password_store->AddLogin(http_form);
WaitForPasswordStore();
// Treat the host of the HTTPS test server as HSTS.
AddHSTSHost(https_test_server().host_port_pair().host());
// Navigate to HTTPS page and trigger the migration.
ui_test_utils::NavigateToURL(
browser(), https_test_server().GetURL("/password/done.html"));
// Call the API to trigger the account chooser.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(), "navigator.credentials.get({password: true})"));
BubbleObserver(WebContents()).WaitForAccountChooser();
// Wait for the migration logic to actually touch the password store.
WaitForPasswordStore();
// Only HTTPS passwords should be present.
EXPECT_TRUE(
password_store->stored_passwords().at(http_origin.spec()).empty());
EXPECT_FALSE(
password_store->stored_passwords().at(https_origin.spec()).empty());
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
AutoSigninOldCredentialAndNavigation) {
// Save credentials with 'skip_zero_click' false.
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS).get());
autofill::PasswordForm signin_form;
signin_form.signon_realm = embedded_test_server()->base_url().spec();
signin_form.password_value = base::ASCIIToUTF16("password");
signin_form.username_value = base::ASCIIToUTF16("user");
signin_form.origin = embedded_test_server()->base_url();
signin_form.skip_zero_click = false;
password_store->AddLogin(signin_form);
// Enable 'auto signin' for the profile.
password_bubble_experiment::RecordAutoSignInPromptFirstRunExperienceWasShown(
browser()->profile()->GetPrefs());
NavigateToFile("/password/password_form.html");
std::string fill_password =
"document.getElementById('username_field').value = 'trash';"
"document.getElementById('password_field').value = 'trash';";
ASSERT_TRUE(content::ExecuteScript(RenderViewHost(), fill_password));
// Call the API to trigger the notification to the client.
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"navigator.credentials.get({password: true})"
".then(cred => window.location = '/password/done.html');"));
NavigationObserver observer(WebContents());
observer.SetPathToWaitFor("/password/done.html");
observer.Wait();
BubbleObserver prompt_observer(WebContents());
// The autofill password manager shouldn't react to the successful login
// because it was suppressed when the site got the credential back.
EXPECT_FALSE(prompt_observer.IsSavePromptShownAutomatically());
}
// Regression test for https://crbug.com/736357.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreInUnloadHandler_SameSite_OnDemandMojoPipe) {
TestStoreInUnloadHandlerForSameSiteNavigation(
false /* preestablish_mojo_pipe */);
}
// Regression test for https://crbug.com/736357.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreInUnloadHandler_SameSite_PreestablishedPipe) {
TestStoreInUnloadHandlerForSameSiteNavigation(
true /* preestablish_mojo_pipe */);
}
// Regression test for https://crbug.com/736357.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreInUnloadHandler_CrossSite_OnDemandMojoPipe) {
TestStoreInUnloadHandlerForCrossSiteNavigation(
false /* preestablish_mojo_pipe */);
}
// Regression test for https://crbug.com/736357.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
StoreInUnloadHandler_CrossSite_PreestablishedPipe) {
TestStoreInUnloadHandlerForCrossSiteNavigation(
true /* preestablish_mojo_pipe */);
}
// Regression test for https://crbug.com/736357.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
MojoConnectionRecreatedAfterNavigation) {
const GURL a_url1 = https_test_server().GetURL("a.com", "/title1.html");
const GURL a_url2 = https_test_server().GetURL("a.com", "/title2.html");
const GURL a_url2_ref = https_test_server().GetURL("a.com", "/title2.html#r");
const GURL b_url = https_test_server().GetURL("b.com", "/title2.html#ref");
// Enable 'auto signin' for the profile.
password_bubble_experiment::RecordAutoSignInPromptFirstRunExperienceWasShown(
browser()->profile()->GetPrefs());
// Navigate to a mostly empty page.
ui_test_utils::NavigateToURL(browser(), a_url1);
ChromePasswordManagerClient* client =
ChromePasswordManagerClient::FromWebContents(WebContents());
// Store a credential, and expect it to establish the Mojo connection.
EXPECT_FALSE(client->has_binding_for_credential_manager());
EXPECT_FALSE(client->was_store_ever_called());
ASSERT_TRUE(content::ExecuteScript(
WebContents(),
"var c = new PasswordCredential({ id: 'user', password: 'hunter2' });"
"navigator.credentials.store(c);"));
BubbleObserver prompt_observer(WebContents());
prompt_observer.WaitForAutomaticSavePrompt();
ASSERT_TRUE(prompt_observer.IsSavePromptShownAutomatically());
prompt_observer.AcceptSavePrompt();
WaitForPasswordStore();
EXPECT_TRUE(client->has_binding_for_credential_manager());
EXPECT_TRUE(client->was_store_ever_called());
// Trigger a same-site navigation.
content::RenderFrameHost* old_rfh = WebContents()->GetMainFrame();
ui_test_utils::NavigateToURL(browser(), a_url2);
ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
// Expect the Mojo connection closed.
EXPECT_FALSE(client->has_binding_for_credential_manager());
// Calling navigator.credentials.get() again should re-establish the Mojo
// connection and succeed.
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
true));
EXPECT_TRUE(client->has_binding_for_credential_manager());
// Same-document navigation. Call to get() succeeds.
ui_test_utils::NavigateToURL(browser(), a_url2_ref);
ASSERT_EQ(old_rfh, WebContents()->GetMainFrame());
EXPECT_TRUE(client->has_binding_for_credential_manager());
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
true));
// Cross-site navigation. Call to get() succeeds without results.
ui_test_utils::NavigateToURL(browser(), b_url);
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
false));
// Trigger a cross-site navigation back. Call to get() should still succeed,
// and once again with results.
ui_test_utils::NavigateToURL(browser(), a_url1);
ASSERT_NO_FATAL_FAILURE(
TriggerNavigatorGetPasswordCredentialsAndExpectHasResult(WebContents(),
true));
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest, SaveViaAPIAndAutofill) {
NavigateToFile("/password/password_form.html");
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"document.getElementById('input_submit_button').addEventListener('click',"
"function(event) {"
"var c = new PasswordCredential({ id: 'user', password: 'API' });"
"navigator.credentials.store(c);"
"});"));
// Fill the password and click the button to submit the page. The API should
// suppress the autofill password manager.
NavigationObserver form_submit_observer(WebContents());
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"document.getElementById('username_field').value = 'user';"
"document.getElementById('password_field').value = 'autofill';"
"document.getElementById('input_submit_button').click();"));
form_submit_observer.Wait();
WaitForPasswordStore();
BubbleObserver prompt_observer(WebContents());
ASSERT_TRUE(prompt_observer.IsSavePromptShownAutomatically());
prompt_observer.AcceptSavePrompt();
WaitForPasswordStore();
password_manager::TestPasswordStore::PasswordMap stored =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get())->stored_passwords();
ASSERT_EQ(1u, stored.size());
autofill::PasswordForm signin_form = stored.begin()->second[0];
EXPECT_EQ(base::ASCIIToUTF16("user"), signin_form.username_value);
EXPECT_EQ(base::ASCIIToUTF16("API"), signin_form.password_value);
EXPECT_EQ(embedded_test_server()->base_url().spec(),
signin_form.signon_realm);
EXPECT_EQ(embedded_test_server()->base_url(), signin_form.origin);
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest, UpdateViaAPIAndAutofill) {
// Save credentials with 'skip_zero_click' false.
scoped_refptr<password_manager::TestPasswordStore> password_store =
static_cast<password_manager::TestPasswordStore*>(
PasswordStoreFactory::GetForProfile(
browser()->profile(), ServiceAccessType::IMPLICIT_ACCESS)
.get());
autofill::PasswordForm signin_form;
signin_form.signon_realm = embedded_test_server()->base_url().spec();
signin_form.password_value = base::ASCIIToUTF16("old_pass");
signin_form.username_value = base::ASCIIToUTF16("user");
signin_form.origin = embedded_test_server()->base_url();
signin_form.skip_zero_click = true;
signin_form.preferred = true;
password_store->AddLogin(signin_form);
NavigateToFile("/password/password_form.html");
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"document.getElementById('input_submit_button').addEventListener('click',"
"function(event) {"
"var c = new PasswordCredential({ id: 'user', password: 'API' });"
"navigator.credentials.store(c);"
"});"));
// Fill the new password and click the button to submit the page later. The
// API should suppress the autofill password manager and overwrite the
// password.
NavigationObserver form_submit_observer(WebContents());
ASSERT_TRUE(content::ExecuteScript(
RenderViewHost(),
"document.getElementById('username_field').value = 'user';"
"document.getElementById('password_field').value = 'autofill';"
"document.getElementById('input_submit_button').click();"));
form_submit_observer.Wait();
// Wait for the password store before checking the prompt because it pops up
// after the store replies.
WaitForPasswordStore();
BubbleObserver prompt_observer(WebContents());
EXPECT_FALSE(prompt_observer.IsSavePromptShownAutomatically());
EXPECT_FALSE(prompt_observer.IsUpdatePromptShownAutomatically());
signin_form.skip_zero_click = false;
signin_form.times_used = 1;
signin_form.password_value = base::ASCIIToUTF16("API");
password_manager::TestPasswordStore::PasswordMap stored =
password_store->stored_passwords();
ASSERT_EQ(1u, stored.size());
EXPECT_EQ(signin_form, stored[signin_form.signon_realm][0]);
}
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest, CredentialsAutofilled) {
NavigateToFile("/password/password_form.html");
ASSERT_TRUE(content::ExecuteScript(
RenderFrameHost(),
"var c = new PasswordCredential({ id: 'user', password: '12345' });"
"navigator.credentials.store(c);"));
BubbleObserver bubble_observer(WebContents());
bubble_observer.WaitForAutomaticSavePrompt();
bubble_observer.AcceptSavePrompt();
// Reload the page and make sure it's autofilled.
NavigateToFile("/password/password_form.html");
content::SimulateMouseClickAt(
WebContents(), 0, blink::WebMouseEvent::Button::kLeft, gfx::Point(1, 1));
WaitForElementValue("username_field", "user");
WaitForElementValue("password_field", "12345");
}
// Tests that when navigator.credentials.create() is called with an unsupported
// algorithm, we get a NotSupportedError.
IN_PROC_BROWSER_TEST_F(CredentialManagerBrowserTest,
CreatePublicKeyCredentialAlgorithmNotSupported) {
const GURL a_url1 = https_test_server().GetURL("a.com", "/title1.html");
// Navigate to a mostly empty page.
ui_test_utils::NavigateToURL(browser(), a_url1);
ASSERT_NO_FATAL_FAILURE(
CreatePublicKeyCredentialWithUnsupportedAlgorithmAndExpectNotSupported(
WebContents()));
}
} // namespace