Bug 1915672 - Add Telemetry for OSAuth for Formautofill and about:logins. r=dimi,mtigley,settings-reviewers,firefox-desktop-core-reviewers ,joschmidt

Differential Revision: https://phabricator.services.mozilla.com/D232339
This commit is contained in:
Helena
2025-01-15 21:55:20 +00:00
parent 2023a49747
commit 651b2cfe37
15 changed files with 292 additions and 35 deletions

View File

@@ -3160,6 +3160,21 @@ BrowserGlue.prototype = {
}, },
}, },
{
name: "OS Authentication telemetry",
task: () => {
const osAuthForCc = lazy.FormAutofillUtils.getOSAuthEnabled(
lazy.FormAutofillUtils.AUTOFILL_CREDITCARDS_REAUTH_PREF
);
const osAuthForPw = lazy.LoginHelper.getOSAuthEnabled(
lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
);
Glean.formautofill.osAuthEnabled.set(osAuthForCc);
Glean.pwmgr.osAuthEnabled.set(osAuthForPw);
},
},
{ {
name: "browser-startup-idle-tasks-finished", name: "browser-startup-idle-tasks-finished",
task: () => { task: () => {

View File

@@ -121,13 +121,17 @@ export class AboutLoginsChild extends JSWindowActorChild {
* @param resolve Callback that is called with result of authentication. * @param resolve Callback that is called with result of authentication.
* @param messageId The string ID that corresponds to a string stored in aboutLogins.ftl. * @param messageId The string ID that corresponds to a string stored in aboutLogins.ftl.
* This string will be displayed only when the OS auth dialog is used. * This string will be displayed only when the OS auth dialog is used.
* @param reason The reason for requesting reauthentication, used for telemetry.
*/ */
async promptForPrimaryPassword(resolve, messageId) { async promptForPrimaryPassword(resolve, messageId, reason) {
gPrimaryPasswordPromise = { gPrimaryPasswordPromise = {
resolve, resolve,
}; };
that.sendAsyncMessage("AboutLogins:PrimaryPasswordRequest", messageId); that.sendAsyncMessage("AboutLogins:PrimaryPasswordRequest", {
messageId,
reason,
});
return gPrimaryPasswordPromise; return gPrimaryPasswordPromise;
}, },

View File

@@ -131,7 +131,10 @@ export class AboutLoginsParent extends JSWindowActorParent {
break; break;
} }
case "AboutLogins:PrimaryPasswordRequest": { case "AboutLogins:PrimaryPasswordRequest": {
await this.#primaryPasswordRequest(message.data); await this.#primaryPasswordRequest(
message.data.messageId,
message.data.reason
);
break; break;
} }
case "AboutLogins:Subscribe": { case "AboutLogins:Subscribe": {
@@ -245,7 +248,7 @@ export class AboutLoginsParent extends JSWindowActorParent {
this.#ownerGlobal.openPreferences("privacy-logins"); this.#ownerGlobal.openPreferences("privacy-logins");
} }
async #primaryPasswordRequest(messageId) { async #primaryPasswordRequest(messageId, reason) {
if (!messageId) { if (!messageId) {
throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId."); throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId.");
} }
@@ -277,7 +280,8 @@ export class AboutLoginsParent extends JSWindowActorParent {
isOSAuthEnabled, isOSAuthEnabled,
AboutLogins._authExpirationTime, AboutLogins._authExpirationTime,
messageText.value, messageText.value,
captionText.value captionText.value,
reason
); );
this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", { this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", {
result: isAuthorized, result: isAuthorized,
@@ -394,12 +398,14 @@ export class AboutLoginsParent extends JSWindowActorParent {
]); ]);
} }
let reason = "export_logins";
let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth( let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
this.browsingContext.embedderElement, this.browsingContext.embedderElement,
true, true,
null, // Prompt regardless of a recent prompt null, // Prompt regardless of a recent prompt
messageText.value, messageText.value,
captionText.value captionText.value,
reason
); );
let { name, extra = {}, value = null } = telemetryEvent; let { name, extra = {}, value = null } = telemetryEvent;

View File

@@ -51,9 +51,13 @@ export function setKeyboardAccessForNonDialogElements(enableKeyboardAccess) {
}); });
} }
export function promptForPrimaryPassword(messageId) { export function promptForPrimaryPassword(messageId, reason) {
return new Promise(resolve => { return new Promise(resolve => {
window.AboutLoginsUtils.promptForPrimaryPassword(resolve, messageId); window.AboutLoginsUtils.promptForPrimaryPassword(
resolve,
messageId,
reason
);
}); });
} }

View File

@@ -422,9 +422,11 @@ export default class LoginItem extends HTMLElement {
} }
// We prompt for the primary password when entering edit mode already. // We prompt for the primary password when entering edit mode already.
const reason = "reveal_logins";
if (this._revealCheckbox.checked && !this.dataset.editing) { if (this._revealCheckbox.checked && !this.dataset.editing) {
let primaryPasswordAuth = await promptForPrimaryPassword( let primaryPasswordAuth = await promptForPrimaryPassword(
"about-logins-reveal-password-os-auth-dialog-message" "about-logins-reveal-password-os-auth-dialog-message",
reason
); );
if (!primaryPasswordAuth) { if (!primaryPasswordAuth) {
this._revealCheckbox.checked = false; this._revealCheckbox.checked = false;
@@ -468,8 +470,10 @@ export default class LoginItem extends HTMLElement {
} }
async handleCopyPasswordClick({ currentTarget }) { async handleCopyPasswordClick({ currentTarget }) {
let reason = "copy_logins";
let primaryPasswordAuth = await promptForPrimaryPassword( let primaryPasswordAuth = await promptForPrimaryPassword(
"about-logins-copy-password-os-auth-dialog-message" "about-logins-copy-password-os-auth-dialog-message",
reason
); );
if (!primaryPasswordAuth) { if (!primaryPasswordAuth) {
return; return;
@@ -547,8 +551,10 @@ export default class LoginItem extends HTMLElement {
} }
async handleEditEvent() { async handleEditEvent() {
let reason = "edit_logins";
let primaryPasswordAuth = await promptForPrimaryPassword( let primaryPasswordAuth = await promptForPrimaryPassword(
"about-logins-edit-login-os-auth-dialog-message2" "about-logins-edit-login-os-auth-dialog-message2",
reason
); );
if (!primaryPasswordAuth) { if (!primaryPasswordAuth) {
return; return;

View File

@@ -80,7 +80,7 @@ Object.defineProperty(window, "AboutLoginsUtils", {
setFocus(element) { setFocus(element) {
return element.focus(); return element.focus();
}, },
async promptForPrimaryPassword(resolve, _messageId) { async promptForPrimaryPassword(resolve, _messageId, _reason) {
resolve(true); resolve(true);
}, },
doLoginsMatch(login1, login2) { doLoginsMatch(login1, login2) {

View File

@@ -2984,12 +2984,23 @@ var gPrivacyPane = {
}, },
]); ]);
let win = Services.wm.getMostRecentBrowserWindow(); let win = Services.wm.getMostRecentBrowserWindow();
// Note on Glean collection: because OSKeyStore.ensureLoggedIn() is not wrapped in
// verifyOSAuth(), it will be documenting "success" for unsupported platforms
// and won't record "fail_error", only "fail_user_canceled"
let loggedIn = await OSKeyStore.ensureLoggedIn( let loggedIn = await OSKeyStore.ensureLoggedIn(
messageText.value, messageText.value,
captionText.value, captionText.value,
win, win,
false false
); );
const result = loggedIn.authenticated ? "success" : "fail_user_canceled";
Glean.pwmgr.promptShownOsReauth.record({
trigger: "toggle_pref_primary_password",
result,
});
if (!loggedIn.authenticated) { if (!loggedIn.authenticated) {
return; return;
} }
@@ -3091,10 +3102,20 @@ var gPrivacyPane = {
osReauthCheckbox.ownerGlobal.docShell.chromeEventHandler.ownerGlobal; osReauthCheckbox.ownerGlobal.docShell.chromeEventHandler.ownerGlobal;
// Calling OSKeyStore.ensureLoggedIn() instead of LoginHelper.verifyOSAuth() // Calling OSKeyStore.ensureLoggedIn() instead of LoginHelper.verifyOSAuth()
// since we want to authenticate user each time this stting is changed. // since we want to authenticate user each time this setting is changed.
// Note on Glean collection: because OSKeyStore.ensureLoggedIn() is not wrapped in
// verifyOSAuth(), it will be documenting "success" for unsupported platforms
// and won't record "fail_error", only "fail_user_canceled"
let isAuthorized = ( let isAuthorized = (
await OSKeyStore.ensureLoggedIn(messageText, captionText, win, false) await OSKeyStore.ensureLoggedIn(messageText, captionText, win, false)
).authenticated; ).authenticated;
Glean.pwmgr.promptShownOsReauth.record({
trigger: "toggle_pref_os_auth",
result: isAuthorized ? "success" : "fail_user_canceled",
});
if (!isAuthorized) { if (!isAuthorized) {
osReauthCheckbox.checked = !osReauthCheckbox.checked; osReauthCheckbox.checked = !osReauthCheckbox.checked;
return; return;
@@ -3105,6 +3126,10 @@ var gPrivacyPane = {
LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF, LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF,
osReauthCheckbox.checked osReauthCheckbox.checked
); );
Glean.pwmgr.requireOsReauthToggle.record({
toggle_state: osReauthCheckbox.checked,
});
}, },
_initOSAuthentication() { _initOSAuthentication() {

View File

@@ -378,15 +378,28 @@ export class ManageCreditCards extends ManageRecords {
"autofill-edit-payment-method-os-prompt-windows", "autofill-edit-payment-method-os-prompt-windows",
"autofill-edit-payment-method-os-prompt-other" "autofill-edit-payment-method-os-prompt-other"
); );
let verified;
const verified = await lazy.FormAutofillUtils.verifyUserOSAuth( let result;
try {
verified = await lazy.FormAutofillUtils.verifyUserOSAuth(
FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF, FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF,
promptMessage promptMessage
); );
result = verified ? "success" : "fail_user_canceled";
} catch (ex) {
result = "fail_error";
throw ex;
} finally {
Glean.formautofill.promptShownOsReauth.record({
trigger: "edit",
result,
});
}
if (!verified) { if (!verified) {
return; return;
} }
} }
let decryptedCCNumObj = {}; let decryptedCCNumObj = {};
if (creditCard && creditCard["cc-number-encrypted"]) { if (creditCard && creditCard["cc-number-encrypted"]) {
try { try {

View File

@@ -335,8 +335,13 @@ FormAutofillPreferences.prototype = {
"autofill-creditcard-os-auth-dialog-caption" "autofill-creditcard-os-auth-dialog-caption"
); );
let win = target.ownerGlobal.docShell.chromeEventHandler.ownerGlobal; let win = target.ownerGlobal.docShell.chromeEventHandler.ownerGlobal;
// Calling OSKeyStore.ensureLoggedIn() instead of FormAutofillUtils.verifyOSAuth() // Calling OSKeyStore.ensureLoggedIn() instead of FormAutofillUtils.verifyOSAuth()
// since we want to authenticate user each time this stting is changed. // since we want to authenticate user each time this setting is changed.
// Note on Glean collection: because OSKeyStore.ensureLoggedIn() is not wrapped in
// verifyOSAuth(), it will be documenting "success" for unsupported platforms
// and won't record "fail_error", only "fail_user_canceled"
let isAuthorized = ( let isAuthorized = (
await lazy.OSKeyStore.ensureLoggedIn( await lazy.OSKeyStore.ensureLoggedIn(
messageText, messageText,
@@ -345,6 +350,11 @@ FormAutofillPreferences.prototype = {
false false
) )
).authenticated; ).authenticated;
Glean.formautofill.promptShownOsReauth.record({
trigger: "toggle_pref_os_auth",
result: isAuthorized ? "success" : "fail_user_canceled",
});
if (!isAuthorized) { if (!isAuthorized) {
target.checked = !target.checked; target.checked = !target.checked;
break; break;
@@ -355,6 +365,9 @@ FormAutofillPreferences.prototype = {
AUTOFILL_CREDITCARDS_REAUTH_PREF, AUTOFILL_CREDITCARDS_REAUTH_PREF,
target.checked target.checked
); );
Glean.formautofill.requireOsReauthToggle.record({
toggle_state: target.checked,
});
} else if (target == this.refs.savedAddressesBtn) { } else if (target == this.refs.savedAddressesBtn) {
target.ownerGlobal.gSubDialog.open(MANAGE_ADDRESSES_URL); target.ownerGlobal.gSubDialog.open(MANAGE_ADDRESSES_URL);
} else if (target == this.refs.savedCreditCardsBtn) { } else if (target == this.refs.savedCreditCardsBtn) {

View File

@@ -342,6 +342,69 @@ formautofill:
expires: never expires: never
telemetry_mirror: FORMAUTOFILL_AVAILABILITY telemetry_mirror: FORMAUTOFILL_AVAILABILITY
prompt_shown_os_reauth:
type: event
description: >
Captures the method of user interaction initiating the prompt and the result of the prompt.
Possible triggers include:
"autofill" when a user attempts to autofill a payment method.
"edit" when a user edits a login in about:preferences
"toggle_pref_os_auth" when a user toggles "Autofill / Require device sign in" in about:preferences
Possible results include:
"success" should be used when the user is shown the OS Auth prompt and successfully authenticates.
"fail_user_canceled" should be used when the user cancels the authentication prompt. The user may or may not have provided an incorrect password before cancelling.
"fail_error" should be used when an unexpected exception is encountered.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
extra_keys:
trigger:
description: >
Which user interaction triggered the event.
type: string
result:
description: >
The result of the OS Authentication.
type: string
require_os_reauth_toggle:
type: event
description: >
Toggle states include:
True means the OS Auth preference is enabled.
False means it is disabled.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
extra_keys:
toggle_state:
description: >
The toggle state after the event.
type: boolean
os_auth_enabled:
type: boolean
description: >
Check at startup whether OS Authentication has been enabled for credit cards.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
address: address:
show_capture_doorhanger: show_capture_doorhanger:
type: event type: event

View File

@@ -606,16 +606,27 @@ export class FormAutofillCreditCardSection extends FormAutofillSection {
"autofill-use-payment-method-os-prompt-windows", "autofill-use-payment-method-os-prompt-windows",
"autofill-use-payment-method-os-prompt-other" "autofill-use-payment-method-os-prompt-other"
); );
const decrypted = await this.getDecryptedString( let decrypted;
let result;
try {
decrypted = await this.getDecryptedString(
profile["cc-number-encrypted"], profile["cc-number-encrypted"],
promptMessage promptMessage
); );
result = decrypted ? "success" : "fail_user_canceled";
} catch (ex) {
result = "fail_error";
throw ex;
} finally {
Glean.formautofill.promptShownOsReauth.record({
trigger: "autofill",
result,
});
}
if (!decrypted) { if (!decrypted) {
// Early return if the decrypted is empty or undefined // Early return if the decrypted is empty or undefined
return false; return false;
} }
profile["cc-number"] = decrypted; profile["cc-number"] = decrypted;
} }
return true; return true;

View File

@@ -1599,13 +1599,15 @@ export const LoginHelper = {
* @param expirationTime Optional timestamp indicating next required re-authentication * @param expirationTime Optional timestamp indicating next required re-authentication
* @param messageText Formatted and localized string to be displayed when the OS auth dialog is used. * @param messageText Formatted and localized string to be displayed when the OS auth dialog is used.
* @param captionText Formatted and localized string to be displayed when the OS auth dialog is used. * @param captionText Formatted and localized string to be displayed when the OS auth dialog is used.
* @param reason The reason for requesting reauthentication, used for telemetry.
*/ */
async requestReauth( async requestReauth(
browser, browser,
OSReauthEnabled, OSReauthEnabled,
expirationTime, expirationTime,
messageText, messageText,
captionText captionText,
reason
) { ) {
let isAuthorized = false; let isAuthorized = false;
let telemetryEvent; let telemetryEvent;
@@ -1644,13 +1646,25 @@ export const LoginHelper = {
} }
// Use the OS auth dialog if there is no primary password // Use the OS auth dialog if there is no primary password
if (!token.hasPassword && OSReauthEnabled) { if (!token.hasPassword && OSReauthEnabled) {
let isAuthorized = await this.verifyUserOSAuth( let result;
try {
isAuthorized = await this.verifyUserOSAuth(
OS_AUTH_FOR_PASSWORDS_PREF, OS_AUTH_FOR_PASSWORDS_PREF,
messageText, messageText,
captionText, captionText,
browser.ownerGlobal, browser.ownerGlobal,
false false
); );
result = isAuthorized ? "success" : "fail_user_canceled";
} catch (ex) {
result = "fail_error";
throw ex;
} finally {
Glean.pwmgr.promptShownOsReauth.record({
trigger: reason,
result,
});
}
let value = lazy.OSKeyStore.canReauth() let value = lazy.OSKeyStore.canReauth()
? "success" ? "success"
: "success_unsupported_platform"; : "success_unsupported_platform";

View File

@@ -377,6 +377,78 @@ pwmgr:
extra_keys: *pwmgr_reauthenticate_extra extra_keys: *pwmgr_reauthenticate_extra
telemetry_mirror: Pwmgr_Reauthenticate_OsAuth telemetry_mirror: Pwmgr_Reauthenticate_OsAuth
prompt_shown_os_reauth:
type: event
description: >
Captures the method of user interaction initiating the prompt and the result of the prompt.
Possible triggers include:
"copy_logins" when a user copies a password in about:logins
"copy_cpm" when a user copies a password in contextual password manager
"edit_logins" when a user edits a login in about:logins
"edit_cpm" when a user edits a login in contextual password manager
"export_logins" when a user exports all passwords in about:logins
"export_cpm" when a user exports all passwords in contextual password manager
"reveal_logins" when a user reveals a password a login in about:logins
"reveal_cpm" when a user reveals a password a login in contextual password manager
"toggle_pref_primary_password" when a user toggles "Use a Primary Password" in about:preferences
"toggle_pref_os_auth" when a user toggles "Passwords / Require device sign in" in about:preferences
Possible results include:
"success" should be used when the user is shown the OS Auth prompt and successfully authenticates.
"fail_user_canceled" should be used when the user cancels the authentication prompt. The user may or may not have provided an incorrect password before cancelling.
"fail_error" should be used when an unexpected exception is encountered.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
extra_keys:
trigger:
description: >
Which user interaction triggered the event.
type: string
result:
description: >
The result of the OS Authentication.
type: string
require_os_reauth_toggle:
type: event
description: >
Captures the state of the toggle after the change was made.
Toggle states include:
True means the OS Auth preference is enabeled.
False means it is disabled.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
extra_keys:
toggle_state:
description: >
The toggle state after the event.
type: boolean
os_auth_enabled:
type: boolean
description: >
Check at startup whether OS Authentication has been enabled for passwords.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1915672
notification_emails:
- autofill@lists.mozilla.org
- passwords-dev@mozilla.org
expires: never
open_management_aboutprotections: open_management_aboutprotections:
type: event type: event
description: > description: >

View File

@@ -197,12 +197,21 @@ export class MegalistViewModel {
} }
async #promptForReauth(command) { async #promptForReauth(command) {
// used for recording telemetry
const reasonMap = {
Copy: "copy_cpm",
Reveal: "reveal_cpm",
Edit: "edit_cpm",
};
const reason = reasonMap[command.id];
const { isAuthorized } = await lazy.LoginHelper.requestReauth( const { isAuthorized } = await lazy.LoginHelper.requestReauth(
lazy.BrowserWindowTracker.getTopWindow().gBrowser, lazy.BrowserWindowTracker.getTopWindow().gBrowser,
this.getOSAuthEnabled(), this.getOSAuthEnabled(),
this.#authExpirationTime, this.#authExpirationTime,
command.OSAuthPromptMessage, command.OSAuthPromptMessage,
command.OSAuthCaptionMessage command.OSAuthCaptionMessage,
reason
); );
if (isAuthorized) { if (isAuthorized) {

View File

@@ -519,12 +519,14 @@ export class LoginDataSource extends DataSourceBase {
LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
); );
const reason = "export_cpm";
let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth( let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth(
browsingContext, browsingContext,
isOSAuthEnabled, isOSAuthEnabled,
null, // Prompt regardless of a recent prompt null, // Prompt regardless of a recent prompt
this.#exportPasswordsStrings.OSReauthMessage, this.#exportPasswordsStrings.OSReauthMessage,
this.#exportPasswordsStrings.OSAuthDialogCaption this.#exportPasswordsStrings.OSAuthDialogCaption,
reason
); );
let { name, extra = {}, value = null } = telemetryEvent; let { name, extra = {}, value = null } = telemetryEvent;