Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/fxa-auth-server/lib/metrics/glean/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ jest.mock('./server_events', () => ({
recordLoginConfirmSkipForKnownDevice: mockFn(
'recordLoginConfirmSkipForKnownDevice'
),
recordPasskeyAuthenticationStarted: mockFn(
'recordPasskeyAuthenticationStarted'
),
recordPasskeyAuthenticationVerificationSuccess: mockFn(
'recordPasskeyAuthenticationVerificationSuccess'
),
recordPasskeyCreateComplete: mockFn('recordPasskeyCreateComplete'),
recordPasskeyDeleteSuccess: mockFn('recordPasskeyDeleteSuccess'),
recordPasskeyRenameSuccess: mockFn('recordPasskeyRenameSuccess'),
Expand Down
4 changes: 4 additions & 0 deletions packages/fxa-auth-server/lib/metrics/glean/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ export function gleanMetrics(config: ConfigType) {
knownDevice: createEventFn('login_confirm_skip_for_known_device'),
},
passkey: {
authenticationStarted: createEventFn('passkey_authentication_started'),
authenticationVerificationSuccess: createEventFn(
'passkey_authentication_verification_success'
),
createComplete: createEventFn('passkey_create_complete'),
deleteSuccess: createEventFn('passkey_delete_success'),
renameSuccess: createEventFn('passkey_rename_success'),
Expand Down
172 changes: 168 additions & 4 deletions packages/fxa-auth-server/lib/metrics/glean/server_events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* 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/. */

// AUTOGENERATED BY glean_parser v19.0.0. DO NOT EDIT. DO NOT COMMIT.
// AUTOGENERATED BY glean_parser v19.2.0. DO NOT EDIT. DO NOT COMMIT.

// This requires `uuid` and `mozlog` libraries to be in the environment
// @types/uuid and mozlog types definitions are required in devDependencies
Expand Down Expand Up @@ -146,7 +146,7 @@ class AccountsEventsServerEvent {
},
// `Unknown` fields below are required in the Glean schema, however they are not useful in server context
client_info: {
telemetry_sdk_build: 'glean_parser v19.0.0',
telemetry_sdk_build: 'glean_parser v19.2.0',
first_run_date: 'Unknown',
os: 'Unknown',
os_version: 'Unknown',
Expand Down Expand Up @@ -272,7 +272,7 @@ class EventsServerEventLogger {
},
// `Unknown` fields below are required in the Glean schema, however they are not useful in server context
client_info: {
telemetry_sdk_build: 'glean_parser v19.0.0',
telemetry_sdk_build: 'glean_parser v19.2.0',
first_run_date: 'Unknown',
os: 'Unknown',
os_version: 'Unknown',
Expand Down Expand Up @@ -2194,7 +2194,7 @@ class EventsServerEventLogger {
* @param {string} utm_medium - The "medium" on which the user acted. For example, if the user clicked on a link in an email, then the value of this metric would be 'email'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_source - The source from where the user started. For example, if the user clicked on a link on the Mozilla accounts web site, this value could be 'fx-website'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_term - This metric is similar to the `utm.source`; it is used in the Firefox browser. For example, if the user started from about:welcome, then the value could be 'aboutwelcome-default-screen'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} reason - Indicates how the user completed login. Values include "email" (traditional email/password), "otp" (passwordless OTP code), "google" (third-party login via Google), "apple" (third-party login via Apple)..
* @param {string} reason - Indicates how the user completed login. Values include "email" (traditional email/password), "otp" (passwordless OTP code), "google" (third-party login via Google), "apple" (third-party login via Apple), "passkey" (WebAuthn passkey)..
*/
recordLoginComplete({
user_agent,
Expand Down Expand Up @@ -3086,6 +3086,170 @@ class EventsServerEventLogger {
event,
});
}
/**
* Record and submit a passkey_authentication_started event:
* Passkey authentication (assertion) ceremony started on the server: a challenge was generated for the client. Pairs with the login.complete (reason=passkey) completion signal to measure the server-side passkey sign-in funnel.
* Event is logged using internal mozlog logger.
*
* @param {string} user_agent - The user agent.
* @param {string} ip_address - The IP address. Will be used to decode Geo
* information and scrubbed at ingestion.
* @param {string} account_user_id - The firefox/mozilla account id.
* @param {string} account_user_id_sha256 - A hex string of a sha256 hash of the account's uid.
* @param {string} relying_party_oauth_client_id - The client id of the relying party.
* @param {string} relying_party_service - The service name of the relying party.
* @param {string} session_device_type - one of 'mobile', 'tablet', or ''.
* @param {string} session_entrypoint - Entrypoint to the service.
* @param {string} session_entrypoint_experiment - Identifier for the experiment the user is part of at the entrypoint.
* @param {string} session_entrypoint_variation - Identifier for the experiment variation the user is part of at the entrypoint.
* @param {string} session_flow_id - an ID generated by FxA for its flow metrics.
* @param {string} utm_campaign - A marketing campaign. For example, if a user signs into FxA from selecting a Mozilla VPN plan on Mozilla VPN's product site, then the value of this metric could be 'vpn-product-page'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters. The special value of 'page+referral+-+not+part+of+a+campaign' is also allowed..
* @param {string} utm_content - The content on which the user acted. For example, if the user clicked on the (previously available) "Get started here" link in "Looking for Firefox Sync? Get started here", then the value for this metric would be 'fx-sync-get-started'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_medium - The "medium" on which the user acted. For example, if the user clicked on a link in an email, then the value of this metric would be 'email'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_source - The source from where the user started. For example, if the user clicked on a link on the Mozilla accounts web site, this value could be 'fx-website'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_term - This metric is similar to the `utm.source`; it is used in the Firefox browser. For example, if the user started from about:welcome, then the value could be 'aboutwelcome-default-screen'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
*/
recordPasskeyAuthenticationStarted({
user_agent,
ip_address,
account_user_id,
account_user_id_sha256,
relying_party_oauth_client_id,
relying_party_service,
session_device_type,
session_entrypoint,
session_entrypoint_experiment,
session_entrypoint_variation,
session_flow_id,
utm_campaign,
utm_content,
utm_medium,
utm_source,
utm_term,
}: {
user_agent: string;
ip_address: string;
account_user_id: string;
account_user_id_sha256: string;
relying_party_oauth_client_id: string;
relying_party_service: string;
session_device_type: string;
session_entrypoint: string;
session_entrypoint_experiment: string;
session_entrypoint_variation: string;
session_flow_id: string;
utm_campaign: string;
utm_content: string;
utm_medium: string;
utm_source: string;
utm_term: string;
}) {
const event = {
category: 'passkey',
name: 'authentication_started',
};
this.#record({
user_agent,
ip_address,
account_user_id,
account_user_id_sha256,
relying_party_oauth_client_id,
relying_party_service,
session_device_type,
session_entrypoint,
session_entrypoint_experiment,
session_entrypoint_variation,
session_flow_id,
utm_campaign,
utm_content,
utm_medium,
utm_source,
utm_term,
event,
});
}
/**
* Record and submit a passkey_authentication_verification_success event:
* Passkey authentication assertion verified successfully on the server, before any session/token work. Sits between passkey.authentication_started and login.complete (reason=passkey) so failures during token/session creation are distinguishable from a failed WebAuthn ceremony.
* Event is logged using internal mozlog logger.
*
* @param {string} user_agent - The user agent.
* @param {string} ip_address - The IP address. Will be used to decode Geo
* information and scrubbed at ingestion.
* @param {string} account_user_id - The firefox/mozilla account id.
* @param {string} account_user_id_sha256 - A hex string of a sha256 hash of the account's uid.
* @param {string} relying_party_oauth_client_id - The client id of the relying party.
* @param {string} relying_party_service - The service name of the relying party.
* @param {string} session_device_type - one of 'mobile', 'tablet', or ''.
* @param {string} session_entrypoint - Entrypoint to the service.
* @param {string} session_entrypoint_experiment - Identifier for the experiment the user is part of at the entrypoint.
* @param {string} session_entrypoint_variation - Identifier for the experiment variation the user is part of at the entrypoint.
* @param {string} session_flow_id - an ID generated by FxA for its flow metrics.
* @param {string} utm_campaign - A marketing campaign. For example, if a user signs into FxA from selecting a Mozilla VPN plan on Mozilla VPN's product site, then the value of this metric could be 'vpn-product-page'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters. The special value of 'page+referral+-+not+part+of+a+campaign' is also allowed..
* @param {string} utm_content - The content on which the user acted. For example, if the user clicked on the (previously available) "Get started here" link in "Looking for Firefox Sync? Get started here", then the value for this metric would be 'fx-sync-get-started'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_medium - The "medium" on which the user acted. For example, if the user clicked on a link in an email, then the value of this metric would be 'email'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_source - The source from where the user started. For example, if the user clicked on a link on the Mozilla accounts web site, this value could be 'fx-website'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
* @param {string} utm_term - This metric is similar to the `utm.source`; it is used in the Firefox browser. For example, if the user started from about:welcome, then the value could be 'aboutwelcome-default-screen'. The value has a max length of 128 characters with the alphanumeric characters, _ (underscore), forward slash (/), . (period), % (percentage sign), and - (hyphen) in the allowed set of characters..
*/
recordPasskeyAuthenticationVerificationSuccess({
user_agent,
ip_address,
account_user_id,
account_user_id_sha256,
relying_party_oauth_client_id,
relying_party_service,
session_device_type,
session_entrypoint,
session_entrypoint_experiment,
session_entrypoint_variation,
session_flow_id,
utm_campaign,
utm_content,
utm_medium,
utm_source,
utm_term,
}: {
user_agent: string;
ip_address: string;
account_user_id: string;
account_user_id_sha256: string;
relying_party_oauth_client_id: string;
relying_party_service: string;
session_device_type: string;
session_entrypoint: string;
session_entrypoint_experiment: string;
session_entrypoint_variation: string;
session_flow_id: string;
utm_campaign: string;
utm_content: string;
utm_medium: string;
utm_source: string;
utm_term: string;
}) {
const event = {
category: 'passkey',
name: 'authentication_verification_success',
};
this.#record({
user_agent,
ip_address,
account_user_id,
account_user_id_sha256,
relying_party_oauth_client_id,
relying_party_service,
session_device_type,
session_entrypoint,
session_entrypoint_experiment,
session_entrypoint_variation,
session_flow_id,
utm_campaign,
utm_content,
utm_medium,
utm_source,
utm_term,
event,
});
}
/**
* Record and submit a passkey_create_complete event:
* Passkey registration completed successfully on the server
Expand Down
45 changes: 45 additions & 0 deletions packages/fxa-auth-server/lib/routes/passkeys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ describe('passkeys routes', () => {
};
glean = {
passkey: {
authenticationStarted: jest.fn(),
authenticationVerificationSuccess: jest.fn(),
createComplete: jest.fn(),
deleteSuccess: jest.fn(),
renameSuccess: jest.fn(),
Expand Down Expand Up @@ -997,6 +999,19 @@ describe('passkeys routes', () => {
).toHaveBeenCalledWith();
});

it('records glean.passkey.authenticationStarted with the request', async () => {
const request = {
auth: { credentials: {} },
app: { ua: {} },
};
await runTest('/passkey/authentication/start', request);

expect(glean.passkey.authenticationStarted).toHaveBeenCalledTimes(1);
expect(glean.passkey.authenticationStarted).toHaveBeenCalledWith(
expect.objectContaining({ auth: { credentials: {} } })
);
});

it('enforces rate limiting via customs.checkIpOnly', async () => {
await runTest('/passkey/authentication/start', {
auth: { credentials: {} },
Expand Down Expand Up @@ -1125,6 +1140,36 @@ describe('passkeys routes', () => {
expect(glean.login.complete).not.toHaveBeenCalled();
});

it('emits glean.passkey.authenticationVerificationSuccess on a verified assertion', async () => {
await runTest('/passkey/authentication/finish', {
auth: { credentials: {} },
app: { ua: {} },
payload,
});

expect(
glean.passkey.authenticationVerificationSuccess
).toHaveBeenCalledTimes(1);
});

it('does not emit glean.passkey.authenticationVerificationSuccess when verification fails', async () => {
mockPasskeyService.verifyAuthenticationResponse = jest
.fn()
.mockRejectedValue(AppError.passkeyAuthenticationFailed());

await expect(() =>
runTest('/passkey/authentication/finish', {
auth: { credentials: {} },
app: { ua: {} },
payload,
})
).rejects.toThrow();

expect(
glean.passkey.authenticationVerificationSuccess
).not.toHaveBeenCalled();
});

it('enforces rate limiting via customs.checkIpOnly', async () => {
await runTest('/passkey/authentication/finish', {
auth: { credentials: {} },
Expand Down
7 changes: 5 additions & 2 deletions packages/fxa-auth-server/lib/routes/passkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,7 @@ export class PasskeyHandler {

const options = await this.service.generateAuthenticationChallenge();

// TODO: FXA-12914 — Glean event name needs to be defined in the Glean schema
// await this.glean.passkey.authenticationStarted(request);
await this.glean.passkey.authenticationStarted(request);

return options;
}
Expand Down Expand Up @@ -410,6 +409,10 @@ export class PasskeyHandler {
throw err;
}

// Assertion verified; fired before token/session work so failures in
// that step are distinguishable from a failed ceremony.
await this.glean.passkey.authenticationVerificationSuccess(request);

const account = await this.db.account(uid);

const sessionToken = await this.createPasskeySessionToken(
Expand Down
23 changes: 20 additions & 3 deletions packages/fxa-settings/src/lib/glean/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,15 +749,15 @@ describe('lib/glean', () => {

it('submits a ping with the passkey_enter_password_engage event name and a reason', async () => {
GleanMetrics.passkeyEnterPassword.engage({
event: { reason: 'login' },
event: { reason: 'signin' },
});
await GleanMetrics.isDone();
sinon.assert.calledOnce(setEventNameStub);
sinon.assert.calledWith(
setEventNameStub,
'passkey_enter_password_engage'
);
sinon.assert.calledWith(setEventReasonStub, 'login');
sinon.assert.calledWith(setEventReasonStub, 'signin');
});

it('submits a ping with the passkey_enter_password_submit event name and a reason', async () => {
Expand Down Expand Up @@ -787,7 +787,7 @@ describe('lib/glean', () => {

it('submits a ping with the passkey_enter_password_success event name and a reason', async () => {
GleanMetrics.passkeyEnterPassword.success({
event: { reason: 'login' },
event: { reason: 'signin' },
});
await GleanMetrics.isDone();
sinon.assert.calledOnce(setEventNameStub);
Expand All @@ -799,12 +799,29 @@ describe('lib/glean', () => {
});

describe('passkey', () => {
it.each(['emailfirst', 'signin', 'otplogin', 'alternative_auth'])(
'submits passkey_button_view with reason=%s',
async (reason) => {
GleanMetrics.passkey.buttonView({ event: { reason } });
await GleanMetrics.isDone();
sinon.assert.calledOnce(setEventNameStub);
sinon.assert.calledWith(setEventNameStub, 'passkey_button_view');
sinon.assert.calledOnce(setEventReasonStub);
sinon.assert.calledWith(setEventReasonStub, reason);
}
);

it.each([
'emailfirst_nopassword',
'emailfirst_withpassword',
'emailfirst_createdpassword',
'signin_nopassword',
'signin_withpassword',
'signin_createdpassword',
'otplogin_nopassword',
'otplogin_createdpassword',
'alternative_auth_nopassword',
'alternative_auth_createdpassword',
])('submits passkey_auth_success with reason=%s', async (reason) => {
GleanMetrics.passkey.authSuccess({ event: { reason } });
await GleanMetrics.isDone();
Expand Down
5 changes: 5 additions & 0 deletions packages/fxa-settings/src/lib/glean/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,11 @@ const recordEventMetric = (
reason: gleanPingMetrics?.event?.['reason'] || '',
});
break;
case 'passkey_button_view':
passkey.buttonView.record({
reason: gleanPingMetrics?.event?.['reason'] || '',
});
break;
case 'passkey_auth_success':
passkey.authSuccess.record({
reason: gleanPingMetrics?.event?.['reason'] || '',
Expand Down
Loading
Loading