Bug 1676216 - Use login storage for HTTP auth on Android r=dimi,owlish

This commit allows Android to use the login storage for HTTP auth by migrating
some common toolkit code to promptUsernameAndPassword and promptPassword which
use the login storage.

Differential Revision: https://phabricator.services.mozilla.com/D122508
This commit is contained in:
Agi Sferro
2021-09-01 23:40:34 +00:00
parent 936c1e5de4
commit 5c1c04a623
8 changed files with 198 additions and 166 deletions

View File

@@ -68,6 +68,12 @@ class LoginStorageDelegate {
GeckoViewAutocomplete.onLoginSave(selectedLogin);
}
);
return {
dismiss() {
prompt.dismiss();
},
};
}
promptToChangePassword(
@@ -105,6 +111,12 @@ class LoginStorageDelegate {
);
}
);
return {
dismiss() {
prompt.dismiss();
},
};
}
promptToChangePasswordWithUsernames(aBrowser, aLogins, aNewLogin) {

View File

@@ -7,6 +7,8 @@ import org.mozilla.geckoview.GeckoSession.NavigationDelegate
import org.mozilla.geckoview.GeckoSession.NavigationDelegate.LoadRequest
import org.mozilla.geckoview.GeckoSession.ProgressDelegate
import org.mozilla.geckoview.GeckoSession.PromptDelegate
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AuthPrompt
import org.mozilla.geckoview.GeckoSession.PromptDelegate.PromptResponse
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
@@ -17,6 +19,7 @@ import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.Autocomplete
@RunWith(AndroidJUnit4::class)
@MediumTest
@@ -103,6 +106,88 @@ class PromptDelegateTest : BaseSessionTest() {
})
}
// This test checks that saved logins are returned to the app when calling onAuthPrompt
@Test fun loginStorageHttpAuthWithPassword() {
mainSession.loadTestPath("/basic-auth/foo/bar")
sessionRule.delegateDuringNextWait(object : Autocomplete.StorageDelegate {
@AssertCalled
override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>>? {
return GeckoResult.fromValue(arrayOf(
Autocomplete.LoginEntry.Builder()
.origin(GeckoSessionTestRule.TEST_ENDPOINT)
.formActionOrigin(GeckoSessionTestRule.TEST_ENDPOINT)
.httpRealm("Fake Realm")
.username("test-username")
.password("test-password")
.formActionOrigin(null)
.guid("test-guid")
.build()
));
}
})
sessionRule.waitUntilCalled(object : PromptDelegate, Autocomplete.StorageDelegate {
@AssertCalled
override fun onAuthPrompt(session: GeckoSession, prompt: AuthPrompt): GeckoResult<PromptResponse>? {
assertThat("Saved login should appear here",
prompt.authOptions.username, equalTo("test-username"))
assertThat("Saved login should appear here",
prompt.authOptions.password, equalTo("test-password"))
return null
}
})
}
// This test checks that we store login information submitted through HTTP basic auth
// This also tests that the login save prompt gets automatically dismissed if
// the login information is incorrect.
@Test fun loginStorageHttpAuth() {
sessionRule.setPrefsUntilTestEnd(mapOf(
"signon.rememberSignons" to true))
val result = GeckoResult<PromptDelegate.BasePrompt>()
val promptInstanceDelegate = object : PromptDelegate.PromptInstanceDelegate {
var prompt: PromptDelegate.BasePrompt? = null
override fun onPromptDismiss(prompt: PromptDelegate.BasePrompt) {
result.complete(prompt)
}
}
sessionRule.delegateUntilTestEnd(object : PromptDelegate, Autocomplete.StorageDelegate {
@AssertCalled
override fun onAuthPrompt(session: GeckoSession, prompt: AuthPrompt): GeckoResult<PromptResponse>? {
return GeckoResult.fromValue(prompt.confirm("foo", "bar"));
}
@AssertCalled
override fun onLoginFetch(domain: String): GeckoResult<Array<Autocomplete.LoginEntry>>? {
return GeckoResult.fromValue(arrayOf());
}
@AssertCalled
override fun onLoginSave(
session: GeckoSession,
request: PromptDelegate.AutocompleteRequest<Autocomplete.LoginSaveOption>
): GeckoResult<PromptResponse>? {
val authInfo = request.options[0].value
assertThat("auth matches", authInfo.formActionOrigin, isEmptyOrNullString())
assertThat("auth matches", authInfo.httpRealm, equalTo("Fake Realm"))
assertThat("auth matches", authInfo.origin, equalTo(GeckoSessionTestRule.TEST_ENDPOINT))
assertThat("auth matches", authInfo.username, equalTo("foo"))
assertThat("auth matches", authInfo.password, equalTo("bar"))
promptInstanceDelegate.prompt = request
request.setDelegate(promptInstanceDelegate)
return GeckoResult()
}
})
mainSession.loadTestPath("/basic-auth/foo/bar")
// The server we try to hit will always reject the login so we should
// get a request to reauth which should dismiss the prompt
val actualPrompt = sessionRule.waitForResult(result)
assertThat("Prompt object should match", actualPrompt, equalTo(promptInstanceDelegate.prompt))
}
@Test fun dismissAuthTest() {
sessionRule.delegateUntilTestEnd(object : PromptDelegate {
@AssertCalled(count = 2)

View File

@@ -13,6 +13,13 @@ const { PromptUtils } = ChromeUtils.import(
"resource://gre/modules/SharedPromptUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"gPrompterService",
"@mozilla.org/login-manager/prompter;1",
Ci.nsILoginManagerPrompter
);
/* eslint-disable block-scoped-var, no-var */
ChromeUtils.defineModuleGetter(
@@ -110,6 +117,7 @@ LoginManagerAuthPromptFactory.prototype = {
// This enables us to consolidate auth prompts with the same browser and
// hashkey (level, origin, realm).
_pendingPrompts: new WeakMap(),
_pendingSavePrompts: new WeakMap(),
// We use a separate bucket for when we don't have a browser.
// _noBrowser -> hashkey -> prompt
_noBrowser: {},
@@ -144,6 +152,15 @@ LoginManagerAuthPromptFactory.prototype = {
return this._pendingPrompts.get(browser)?.get(hashKey);
},
_dismissPendingSavePrompt(browser) {
this._pendingSavePrompts.get(browser)?.dismiss();
this._pendingSavePrompts.delete(browser);
},
_setPendingSavePrompt(browser, prompt) {
this._pendingSavePrompts.set(browser, prompt);
},
_setPendingPrompt(prompt, hashKey) {
let browser = prompt.prompter.browser || this._noBrowser;
let hashToPrompt = this._pendingPrompts.get(browser);
@@ -288,7 +305,6 @@ LoginManagerAuthPrompter.prototype = {
_factory: null,
_chromeWindow: null,
_browser: null,
_openerBrowser: null,
__strBundle: null, // String bundle for L10N
get _strBundle() {
@@ -618,13 +634,23 @@ LoginManagerAuthPrompter.prototype = {
return [formattedOrigin, formattedOrigin + pathname, uri.username];
},
_canPromptToSaveLogin() {
// Cannot prompt if we don't have a window
if (!this._chromeWindow) {
return false;
}
// Can only prompt if we have the prompter service
return !!gPrompterService;
},
async promptAuthInternal(aChannel, aLevel, aAuthInfo) {
var selectedLogin = null;
var checkbox = { value: false };
var checkboxLabel = null;
var epicfail = false;
var canAutologin = false;
var notifyObj;
var canPromptToSave = this._canPromptToSaveLogin();
var foundLogins;
let autofilled = false;
@@ -634,12 +660,12 @@ LoginManagerAuthPrompter.prototype = {
// If the user submits a login but it fails, we need to remove the
// notification prompt that was displayed. Conveniently, the user will
// be prompted for authentication again, which brings us here.
this._removeLoginNotifications();
this._factory._dismissPendingSavePrompt(this._browser);
var [origin, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo);
// Looks for existing logins to prefill the prompt with.
foundLogins = LoginHelper.searchLoginsWithObject({
foundLogins = await Services.logins.searchLoginsAsync({
origin,
httpRealm,
schemeUpgrades: LoginHelper.schemeUpgrades,
@@ -683,9 +709,9 @@ LoginManagerAuthPrompter.prototype = {
canRememberLogin = false;
}
// if checkboxLabel is null, the checkbox won't be shown at all.
notifyObj = this._getPopupNote();
if (canRememberLogin && !notifyObj) {
if (canRememberLogin && !canPromptToSave) {
// If we cannot prompt the user to save the login, we display
// a checkbox on the auth prompt instead.
checkboxLabel = this._getLocalizedString("rememberPassword");
}
} catch (e) {
@@ -752,7 +778,7 @@ LoginManagerAuthPrompter.prototype = {
// determine if the login should be saved. If there isn't a
// notification prompt, only save the login if the user set the
// checkbox to do so.
var rememberLogin = notifyObj ? canRememberLogin : checkbox.value;
var rememberLogin = canPromptToSave ? canRememberLogin : checkbox.value;
if (!ok || !rememberLogin || epicfail) {
return ok;
}
@@ -782,17 +808,13 @@ LoginManagerAuthPrompter.prototype = {
")"
);
if (notifyObj) {
if (canPromptToSave) {
let promptBrowser = LoginHelper.getBrowserForPrompt(browser);
LoginManagerPrompter._showLoginCaptureDoorhanger(
let savePrompt = gPrompterService.promptToSavePassword(
promptBrowser,
newLogin,
"password-save",
{
dismissed: this._inPrivateBrowsing,
}
newLogin
);
Services.obs.notifyObservers(newLogin, "passwordmgr-prompt-save");
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
} else {
Services.logins.addLogin(newLogin);
}
@@ -806,8 +828,14 @@ LoginManagerAuthPrompter.prototype = {
httpRealm +
")"
);
if (notifyObj) {
this._showChangeLoginNotification(browser, selectedLogin, newLogin);
if (canPromptToSave) {
let promptBrowser = LoginHelper.getBrowserForPrompt(browser);
let savePrompt = gPrompterService.promptToChangePassword(
promptBrowser,
selectedLogin,
newLogin
);
this._factory._setPendingSavePrompt(promptBrowser, savePrompt);
} else {
this._updateLogin(selectedLogin, newLogin);
}
@@ -858,7 +886,7 @@ LoginManagerAuthPrompter.prototype = {
// If the user submits a login but it fails, we need to remove the
// notification prompt that was displayed. Conveniently, the user will
// be prompted for authentication again, which brings us here.
this._removeLoginNotifications();
this._factory._dismissPendingSavePrompt(this._browser);
cancelable = this._newAsyncPromptConsumer(aCallback, aContext);
@@ -920,7 +948,6 @@ LoginManagerAuthPrompter.prototype = {
this._chromeWindow = win;
this._browser = browser;
}
this._openerBrowser = null;
this._factory = aFactory || null;
this.log("===== initialized =====");
@@ -934,86 +961,6 @@ LoginManagerAuthPrompter.prototype = {
return this._browser;
},
set openerBrowser(aOpenerBrowser) {
this._openerBrowser = aOpenerBrowser;
},
_removeLoginNotifications() {
var popupNote = this._getPopupNote();
if (popupNote) {
popupNote = popupNote.getNotification("password");
}
if (popupNote) {
popupNote.remove();
}
},
/**
* Shows the Change Password popup notification.
*
* @param aBrowser
* The relevant <browser>.
* @param aOldLogin
* The stored login we want to update.
* @param aNewLogin
* The login object with the changes we want to make.
* @param dismissed
* A boolean indicating if the prompt should be automatically
* dismissed on being shown.
* @param notifySaved
* A boolean value indicating whether the notification should indicate that
* a login has been saved
*/
_showChangeLoginNotification(
aBrowser,
aOldLogin,
aNewLogin,
dismissed = false,
notifySaved = false,
autoSavedLoginGuid = ""
) {
let login = aOldLogin.clone();
login.origin = aNewLogin.origin;
login.formActionOrigin = aNewLogin.formActionOrigin;
login.password = aNewLogin.password;
login.username = aNewLogin.username;
let messageStringID;
if (
aOldLogin.username === "" &&
login.username !== "" &&
login.password == aOldLogin.password
) {
// If the saved password matches the password we're prompting with then we
// are only prompting to let the user add a username since there was one in
// the form. Change the message so the purpose of the prompt is clearer.
messageStringID = "updateLoginMsgAddUsername";
}
let promptBrowser = LoginHelper.getBrowserForPrompt(aBrowser);
LoginManagerPrompter._showLoginCaptureDoorhanger(
promptBrowser,
login,
"password-change",
{
dismissed,
extraAttr: notifySaved ? "attention" : "",
},
{
notifySaved,
messageStringID,
autoSavedLoginGuid,
}
);
let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
Services.obs.notifyObservers(
aNewLogin,
"passwordmgr-prompt-change",
oldGUID
);
},
/* ---------- Internal Methods ---------- */
_updateLogin(login, aNewLogin) {
@@ -1053,48 +1000,6 @@ LoginManagerAuthPrompter.prototype = {
return { win: chromeWin, browser };
},
_getNotifyWindow() {
if (this._openerBrowser) {
let chromeDoc = this._chromeWindow.document.documentElement;
// Check to see if the current window was opened with chrome
// disabled, and if so use the opener window. But if the window
// has been used to visit other pages (ie, has a history),
// assume it'll stick around and *don't* use the opener.
if (chromeDoc.getAttribute("chromehidden") && !this._browser.canGoBack) {
this.log("Using opener window for notification prompt.");
return {
win: this._openerBrowser.ownerGlobal,
browser: this._openerBrowser,
};
}
}
return {
win: this._chromeWindow,
browser: this._browser,
};
},
/**
* Returns the popup notification to this prompter,
* or null if there isn't one available.
*/
_getPopupNote() {
let popupNote = null;
try {
let { win: notifyWin } = this._getNotifyWindow();
// .wrappedJSObject needed here -- see bug 422974 comment 5.
popupNote = notifyWin.wrappedJSObject.PopupNotifications;
} catch (e) {
this.log("Popup notifications not available on window");
}
return popupNote;
},
/**
* The user might enter a login that isn't the one we prefilled, but
* is the same as some other existing login. So, pick a login with a

View File

@@ -138,7 +138,7 @@ class LoginManagerPrompter {
) {
log.debug("promptToSavePassword");
let inPrivateBrowsing = PrivateBrowsingUtils.isBrowserPrivate(aBrowser);
LoginManagerPrompter._showLoginCaptureDoorhanger(
let notification = LoginManagerPrompter._showLoginCaptureDoorhanger(
aBrowser,
aLogin,
"password-save",
@@ -153,6 +153,13 @@ class LoginManagerPrompter {
}
);
Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save");
return {
dismiss() {
let { PopupNotifications } = aBrowser.ownerGlobal.wrappedJSObject;
PopupNotifications.remove(notification);
},
};
}
/**
@@ -786,6 +793,8 @@ class LoginManagerPrompter {
log.debug("Showing the ConfirmationHint");
anchor.ownerGlobal.ConfirmationHint.show(anchor, "passwordSaved");
}
return notification;
}
/**
@@ -840,7 +849,7 @@ class LoginManagerPrompter {
messageStringID = "updateLoginMsgAddUsername2";
}
LoginManagerPrompter._showLoginCaptureDoorhanger(
let notification = LoginManagerPrompter._showLoginCaptureDoorhanger(
aBrowser,
login,
"password-change",
@@ -863,6 +872,13 @@ class LoginManagerPrompter {
"passwordmgr-prompt-change",
oldGUID
);
return {
dismiss() {
let { PopupNotifications } = aBrowser.ownerGlobal.wrappedJSObject;
PopupNotifications.remove(notification);
},
};
}
/**

View File

@@ -24,6 +24,7 @@ XPIDL_SOURCES += [
"nsILoginManagerPrompter.idl",
"nsILoginManagerStorage.idl",
"nsILoginMetaInfo.idl",
"nsIPromptInstance.idl",
]
XPIDL_MODULE = "loginmgr"

View File

@@ -36,13 +36,6 @@ interface nsILoginManagerAuthPrompter : nsISupports {
* This is required if the init function received a chrome window as argument.
*/
attribute Element browser;
/**
* The opener browser that was used to open the window passed to init.
* The opener can be used to determine in which window the prompt
* should be shown.
*/
attribute Element openerBrowser;
};
%{C++

View File

@@ -4,6 +4,7 @@
#include "nsISupports.idl"
#include "nsIPromptInstance.idl"
interface nsILoginInfo;
interface nsIDOMWindow;
@@ -32,12 +33,13 @@ interface nsILoginManagerPrompter : nsISupports {
* Contains values from anything that we think, but are not sure, might be
* a username or password. Has two properties, 'usernames' and 'passwords'.
*/
void promptToSavePassword(in Element aBrowser,
in nsILoginInfo aLogin,
[optional] in boolean dismissed,
[optional] in boolean notifySaved,
[optional] in AString autoFilledLoginGuid,
[optional] in jsval possibleValues);
nsIPromptInstance promptToSavePassword(
in Element aBrowser,
in nsILoginInfo aLogin,
[optional] in boolean dismissed,
[optional] in boolean notifySaved,
[optional] in AString autoFilledLoginGuid,
[optional] in jsval possibleValues);
/**
* Ask the user if they want to change a login's password or username.
@@ -61,14 +63,15 @@ interface nsILoginManagerPrompter : nsISupports {
* Contains values from anything that we think, but are not sure, might be
* a username or password. Has two properties, 'usernames' and 'passwords'.
*/
void promptToChangePassword(in Element aBrowser,
in nsILoginInfo aOldLogin,
in nsILoginInfo aNewLogin,
[optional] in boolean dismissed,
[optional] in boolean notifySaved,
[optional] in AString autoSavedLoginGuid,
[optional] in AString autoFilledLoginGuid,
[optional] in jsval possibleValues);
nsIPromptInstance promptToChangePassword(
in Element aBrowser,
in nsILoginInfo aOldLogin,
in nsILoginInfo aNewLogin,
[optional] in boolean dismissed,
[optional] in boolean notifySaved,
[optional] in AString autoSavedLoginGuid,
[optional] in AString autoFilledLoginGuid,
[optional] in jsval possibleValues);
/**
* Ask the user if they want to change the password for one of
@@ -88,7 +91,7 @@ interface nsILoginManagerPrompter : nsISupports {
* will be set (using the user's selection) before modifyLogin()
* is called.
*/
void promptToChangePasswordWithUsernames(
nsIPromptInstance promptToChangePasswordWithUsernames(
in Element aBrowser,
in Array<nsILoginInfo> logins,
in nsILoginInfo aNewLogin);

View File

@@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
/**
* An object representing a prompt or doorhanger.
*/
[scriptable, uuid(889842e9-052c-46c9-99f3-f4a426571e38)]
interface nsIPromptInstance : nsISupports {
/**
* Dismiss this prompt (e.g. because it's not relevant anymore).
*/
void dismiss();
};