diff --git a/browser/components/BrowserGlue.sys.mjs b/browser/components/BrowserGlue.sys.mjs index b4404f5ecc3b..3cee83911668 100644 --- a/browser/components/BrowserGlue.sys.mjs +++ b/browser/components/BrowserGlue.sys.mjs @@ -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", task: () => { diff --git a/browser/components/aboutlogins/AboutLoginsChild.sys.mjs b/browser/components/aboutlogins/AboutLoginsChild.sys.mjs index 98eaebc51200..ac8b5ea1d7db 100644 --- a/browser/components/aboutlogins/AboutLoginsChild.sys.mjs +++ b/browser/components/aboutlogins/AboutLoginsChild.sys.mjs @@ -121,13 +121,17 @@ export class AboutLoginsChild extends JSWindowActorChild { * @param resolve Callback that is called with result of authentication. * @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. + * @param reason The reason for requesting reauthentication, used for telemetry. */ - async promptForPrimaryPassword(resolve, messageId) { + async promptForPrimaryPassword(resolve, messageId, reason) { gPrimaryPasswordPromise = { resolve, }; - that.sendAsyncMessage("AboutLogins:PrimaryPasswordRequest", messageId); + that.sendAsyncMessage("AboutLogins:PrimaryPasswordRequest", { + messageId, + reason, + }); return gPrimaryPasswordPromise; }, diff --git a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs index d498bdf35ec3..58b0947511cd 100644 --- a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs +++ b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs @@ -131,7 +131,10 @@ export class AboutLoginsParent extends JSWindowActorParent { break; } case "AboutLogins:PrimaryPasswordRequest": { - await this.#primaryPasswordRequest(message.data); + await this.#primaryPasswordRequest( + message.data.messageId, + message.data.reason + ); break; } case "AboutLogins:Subscribe": { @@ -245,7 +248,7 @@ export class AboutLoginsParent extends JSWindowActorParent { this.#ownerGlobal.openPreferences("privacy-logins"); } - async #primaryPasswordRequest(messageId) { + async #primaryPasswordRequest(messageId, reason) { if (!messageId) { throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId."); } @@ -277,7 +280,8 @@ export class AboutLoginsParent extends JSWindowActorParent { isOSAuthEnabled, AboutLogins._authExpirationTime, messageText.value, - captionText.value + captionText.value, + reason ); this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", { result: isAuthorized, @@ -394,12 +398,14 @@ export class AboutLoginsParent extends JSWindowActorParent { ]); } + let reason = "export_logins"; let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth( this.browsingContext.embedderElement, true, null, // Prompt regardless of a recent prompt messageText.value, - captionText.value + captionText.value, + reason ); let { name, extra = {}, value = null } = telemetryEvent; diff --git a/browser/components/aboutlogins/content/aboutLoginsUtils.mjs b/browser/components/aboutlogins/content/aboutLoginsUtils.mjs index 4e55487cecec..c3a58a99eda4 100644 --- a/browser/components/aboutlogins/content/aboutLoginsUtils.mjs +++ b/browser/components/aboutlogins/content/aboutLoginsUtils.mjs @@ -51,9 +51,13 @@ export function setKeyboardAccessForNonDialogElements(enableKeyboardAccess) { }); } -export function promptForPrimaryPassword(messageId) { +export function promptForPrimaryPassword(messageId, reason) { return new Promise(resolve => { - window.AboutLoginsUtils.promptForPrimaryPassword(resolve, messageId); + window.AboutLoginsUtils.promptForPrimaryPassword( + resolve, + messageId, + reason + ); }); } diff --git a/browser/components/aboutlogins/content/components/login-item.mjs b/browser/components/aboutlogins/content/components/login-item.mjs index 0492d1525b45..b03055cfd578 100644 --- a/browser/components/aboutlogins/content/components/login-item.mjs +++ b/browser/components/aboutlogins/content/components/login-item.mjs @@ -422,9 +422,11 @@ export default class LoginItem extends HTMLElement { } // We prompt for the primary password when entering edit mode already. + const reason = "reveal_logins"; if (this._revealCheckbox.checked && !this.dataset.editing) { let primaryPasswordAuth = await promptForPrimaryPassword( - "about-logins-reveal-password-os-auth-dialog-message" + "about-logins-reveal-password-os-auth-dialog-message", + reason ); if (!primaryPasswordAuth) { this._revealCheckbox.checked = false; @@ -468,8 +470,10 @@ export default class LoginItem extends HTMLElement { } async handleCopyPasswordClick({ currentTarget }) { + let reason = "copy_logins"; let primaryPasswordAuth = await promptForPrimaryPassword( - "about-logins-copy-password-os-auth-dialog-message" + "about-logins-copy-password-os-auth-dialog-message", + reason ); if (!primaryPasswordAuth) { return; @@ -547,8 +551,10 @@ export default class LoginItem extends HTMLElement { } async handleEditEvent() { + let reason = "edit_logins"; let primaryPasswordAuth = await promptForPrimaryPassword( - "about-logins-edit-login-os-auth-dialog-message2" + "about-logins-edit-login-os-auth-dialog-message2", + reason ); if (!primaryPasswordAuth) { return; diff --git a/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js b/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js index 80cefd3ddc81..93242062193f 100644 --- a/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js +++ b/browser/components/aboutlogins/tests/chrome/aboutlogins_common.js @@ -80,7 +80,7 @@ Object.defineProperty(window, "AboutLoginsUtils", { setFocus(element) { return element.focus(); }, - async promptForPrimaryPassword(resolve, _messageId) { + async promptForPrimaryPassword(resolve, _messageId, _reason) { resolve(true); }, doLoginsMatch(login1, login2) { diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js index 8d2df764af1a..502988c815f5 100644 --- a/browser/components/preferences/privacy.js +++ b/browser/components/preferences/privacy.js @@ -2984,12 +2984,23 @@ var gPrivacyPane = { }, ]); 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( messageText.value, captionText.value, win, false ); + + const result = loggedIn.authenticated ? "success" : "fail_user_canceled"; + Glean.pwmgr.promptShownOsReauth.record({ + trigger: "toggle_pref_primary_password", + result, + }); + if (!loggedIn.authenticated) { return; } @@ -3091,10 +3102,20 @@ var gPrivacyPane = { osReauthCheckbox.ownerGlobal.docShell.chromeEventHandler.ownerGlobal; // 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 = ( await OSKeyStore.ensureLoggedIn(messageText, captionText, win, false) ).authenticated; + + Glean.pwmgr.promptShownOsReauth.record({ + trigger: "toggle_pref_os_auth", + result: isAuthorized ? "success" : "fail_user_canceled", + }); + if (!isAuthorized) { osReauthCheckbox.checked = !osReauthCheckbox.checked; return; @@ -3105,6 +3126,10 @@ var gPrivacyPane = { LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF, osReauthCheckbox.checked ); + + Glean.pwmgr.requireOsReauthToggle.record({ + toggle_state: osReauthCheckbox.checked, + }); }, _initOSAuthentication() { diff --git a/browser/extensions/formautofill/content/manageDialog.mjs b/browser/extensions/formautofill/content/manageDialog.mjs index 4ae87897d3ad..b6b3f759fe24 100644 --- a/browser/extensions/formautofill/content/manageDialog.mjs +++ b/browser/extensions/formautofill/content/manageDialog.mjs @@ -378,15 +378,28 @@ export class ManageCreditCards extends ManageRecords { "autofill-edit-payment-method-os-prompt-windows", "autofill-edit-payment-method-os-prompt-other" ); - - const verified = await lazy.FormAutofillUtils.verifyUserOSAuth( - FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF, - promptMessage - ); + let verified; + let result; + try { + verified = await lazy.FormAutofillUtils.verifyUserOSAuth( + FormAutofill.AUTOFILL_CREDITCARDS_REAUTH_PREF, + promptMessage + ); + result = verified ? "success" : "fail_user_canceled"; + } catch (ex) { + result = "fail_error"; + throw ex; + } finally { + Glean.formautofill.promptShownOsReauth.record({ + trigger: "edit", + result, + }); + } if (!verified) { return; } } + let decryptedCCNumObj = {}; if (creditCard && creditCard["cc-number-encrypted"]) { try { diff --git a/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs b/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs index 34bca9e1a284..ce09f0c1ee40 100644 --- a/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs +++ b/toolkit/components/formautofill/FormAutofillPreferences.sys.mjs @@ -335,8 +335,13 @@ FormAutofillPreferences.prototype = { "autofill-creditcard-os-auth-dialog-caption" ); let win = target.ownerGlobal.docShell.chromeEventHandler.ownerGlobal; + // 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 = ( await lazy.OSKeyStore.ensureLoggedIn( messageText, @@ -345,6 +350,11 @@ FormAutofillPreferences.prototype = { false ) ).authenticated; + Glean.formautofill.promptShownOsReauth.record({ + trigger: "toggle_pref_os_auth", + result: isAuthorized ? "success" : "fail_user_canceled", + }); + if (!isAuthorized) { target.checked = !target.checked; break; @@ -355,6 +365,9 @@ FormAutofillPreferences.prototype = { AUTOFILL_CREDITCARDS_REAUTH_PREF, target.checked ); + Glean.formautofill.requireOsReauthToggle.record({ + toggle_state: target.checked, + }); } else if (target == this.refs.savedAddressesBtn) { target.ownerGlobal.gSubDialog.open(MANAGE_ADDRESSES_URL); } else if (target == this.refs.savedCreditCardsBtn) { diff --git a/toolkit/components/formautofill/metrics.yaml b/toolkit/components/formautofill/metrics.yaml index a5167882b792..287974646d3d 100644 --- a/toolkit/components/formautofill/metrics.yaml +++ b/toolkit/components/formautofill/metrics.yaml @@ -342,6 +342,69 @@ formautofill: expires: never 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: show_capture_doorhanger: type: event diff --git a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs index 2dac9160e105..42ff0ae01c7d 100644 --- a/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs +++ b/toolkit/components/formautofill/shared/FormAutofillSection.sys.mjs @@ -606,16 +606,27 @@ export class FormAutofillCreditCardSection extends FormAutofillSection { "autofill-use-payment-method-os-prompt-windows", "autofill-use-payment-method-os-prompt-other" ); - const decrypted = await this.getDecryptedString( - profile["cc-number-encrypted"], - promptMessage - ); - + let decrypted; + let result; + try { + decrypted = await this.getDecryptedString( + profile["cc-number-encrypted"], + promptMessage + ); + result = decrypted ? "success" : "fail_user_canceled"; + } catch (ex) { + result = "fail_error"; + throw ex; + } finally { + Glean.formautofill.promptShownOsReauth.record({ + trigger: "autofill", + result, + }); + } if (!decrypted) { // Early return if the decrypted is empty or undefined return false; } - profile["cc-number"] = decrypted; } return true; diff --git a/toolkit/components/passwordmgr/LoginHelper.sys.mjs b/toolkit/components/passwordmgr/LoginHelper.sys.mjs index 2a635d899307..9f93307ec63e 100644 --- a/toolkit/components/passwordmgr/LoginHelper.sys.mjs +++ b/toolkit/components/passwordmgr/LoginHelper.sys.mjs @@ -1599,13 +1599,15 @@ export const LoginHelper = { * @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 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( browser, OSReauthEnabled, expirationTime, messageText, - captionText + captionText, + reason ) { let isAuthorized = false; let telemetryEvent; @@ -1644,13 +1646,25 @@ export const LoginHelper = { } // Use the OS auth dialog if there is no primary password if (!token.hasPassword && OSReauthEnabled) { - let isAuthorized = await this.verifyUserOSAuth( - OS_AUTH_FOR_PASSWORDS_PREF, - messageText, - captionText, - browser.ownerGlobal, - false - ); + let result; + try { + isAuthorized = await this.verifyUserOSAuth( + OS_AUTH_FOR_PASSWORDS_PREF, + messageText, + captionText, + browser.ownerGlobal, + 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() ? "success" : "success_unsupported_platform"; diff --git a/toolkit/components/passwordmgr/metrics.yaml b/toolkit/components/passwordmgr/metrics.yaml index 32682ff5988a..f89eb13c161c 100644 --- a/toolkit/components/passwordmgr/metrics.yaml +++ b/toolkit/components/passwordmgr/metrics.yaml @@ -377,6 +377,78 @@ pwmgr: extra_keys: *pwmgr_reauthenticate_extra 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: type: event description: > diff --git a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs index 2f599801eb1c..3eb2c94ca62d 100644 --- a/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs +++ b/toolkit/components/satchel/megalist/MegalistViewModel.sys.mjs @@ -197,12 +197,21 @@ export class MegalistViewModel { } 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( lazy.BrowserWindowTracker.getTopWindow().gBrowser, this.getOSAuthEnabled(), this.#authExpirationTime, command.OSAuthPromptMessage, - command.OSAuthCaptionMessage + command.OSAuthCaptionMessage, + reason ); if (isAuthorized) { diff --git a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs index 3f5992ab690c..fb72177bf448 100644 --- a/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs +++ b/toolkit/components/satchel/megalist/aggregator/datasources/LoginDataSource.sys.mjs @@ -519,12 +519,14 @@ export class LoginDataSource extends DataSourceBase { LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF ); + const reason = "export_cpm"; let { isAuthorized, telemetryEvent } = await LoginHelper.requestReauth( browsingContext, isOSAuthEnabled, null, // Prompt regardless of a recent prompt this.#exportPasswordsStrings.OSReauthMessage, - this.#exportPasswordsStrings.OSAuthDialogCaption + this.#exportPasswordsStrings.OSAuthDialogCaption, + reason ); let { name, extra = {}, value = null } = telemetryEvent;