From fd007bd9f241524fd66f0d95058ea62cf84f7008 Mon Sep 17 00:00:00 2001 From: Vijay Budhram Date: Thu, 7 May 2026 13:47:17 -0400 Subject: [PATCH] fix(auth): remove push-based sign-in confirmation Because: - HackerOne #3678692 reports that an attacker holding a victim's email and password can register a device on an unverified session with attacker-controlled Web Push encryption keys, then trigger /session/verify/send_push from a duplicated session. The server encrypts the OTP with the attacker's public key and delivers it via AutoPush, allowing the attacker to decrypt the code and verify the session, fully bypassing sign-in confirmation. This commit: - Removes POST /session/verify/send_push and POST /session/verify/verify_push from fxa-auth-server, plus their swagger entries and unit tests. - Removes the verifyLogin push reason and notifyVerifyLoginRequest helper from lib/push.js and the push mock. - Removes sendLoginPushRequest, verifyLoginPushRequest, and the duplicate sendPushLoginRequest from fxa-auth-client. - Deletes the SigninPushCode and SigninPushCodeConfirm pages, routes, query-param model, and PushAuthImage helper from fxa-settings. - Deletes the legacy push-login views, templates, mixin, experiment, router entries, account/client delegate, and react-app route lists in fxa-content-server, and unwires the experiment registration. - Drops the dead smtp.pushVerificationUrl config set. - Removes orphaned session-verify-send-push l10n strings. Email OTP, TOTP, and recovery-code sign-in confirmation paths are unchanged. Hardening of POST /account/device and POST /session/duplicate to require verifiedSessionToken (also flagged in the report as enabling the attack chain) is out of scope here and tracked as a follow-up. --- packages/fxa-auth-client/lib/client.ts | 38 --- packages/fxa-auth-server/config/index.ts | 1 - .../docs/swagger/session-api.ts | 26 -- packages/fxa-auth-server/lib/l10n/server.ftl | 3 - packages/fxa-auth-server/lib/push.js | 21 -- .../fxa-auth-server/lib/routes/session.js | 241 ------------------ .../lib/routes/session.spec.ts | 233 ----------------- packages/fxa-auth-server/test/mocks.js | 1 - .../app/scripts/lib/experiment.js | 1 - .../lib/experiments/grouping-rules/push.js | 61 ----- .../app/scripts/lib/fxa-client.js | 8 - .../app/scripts/lib/router.js | 17 -- .../app/scripts/models/account.js | 8 - .../scripts/templates/push/completed.mustache | 13 - .../templates/push/confirm_login.mustache | 25 -- .../templates/push/send_login.mustache | 21 -- .../mixins/push-login-experiment-mixin.js | 18 -- .../app/scripts/views/mixins/signin-mixin.js | 7 +- .../app/scripts/views/push/completed.js | 22 -- .../app/scripts/views/push/confirm_login.js | 59 ----- .../app/scripts/views/push/send_login.js | 77 ------ .../app/tests/spec/lib/experiment.js | 10 +- .../lib/experiments/grouping-rules/push.js | 50 ---- .../tests/spec/views/mixins/signin-mixin.js | 1 - .../app/tests/spec/views/push/completed.js | 76 ------ .../tests/spec/views/push/confirm_login.js | 123 --------- .../app/tests/spec/views/push/send_login.js | 155 ----------- .../app/tests/test_start.js | 4 - .../routes/react-app/content-server-routes.js | 5 - .../server/lib/routes/react-app/index.js | 2 - .../fxa-settings/src/components/App/index.tsx | 14 - .../images/graphic_push_factor_auth.svg | 4 - .../src/components/images/index.tsx | 10 - .../pages/signin/push-signin-query-params.ts | 22 -- .../Signin/SigninPushCode/container.test.tsx | 235 ----------------- .../pages/Signin/SigninPushCode/container.tsx | 142 ----------- .../src/pages/Signin/SigninPushCode/en.ftl | 8 - .../Signin/SigninPushCode/index.stories.tsx | 17 -- .../Signin/SigninPushCode/index.test.tsx | 66 ----- .../src/pages/Signin/SigninPushCode/index.tsx | 80 ------ .../pages/Signin/SigninPushCode/interfaces.ts | 13 - .../src/pages/Signin/SigninPushCode/mocks.tsx | 76 ------ .../SigninPushCodeConfirm/container.test.tsx | 85 ------ .../SigninPushCodeConfirm/container.tsx | 64 ----- .../pages/Signin/SigninPushCodeConfirm/en.ftl | 9 - .../SigninPushCodeConfirm/greencheck.svg | 11 - .../SigninPushCodeConfirm/index.stories.tsx | 21 -- .../Signin/SigninPushCodeConfirm/index.tsx | 168 ------------ .../Signin/SigninPushCodeConfirm/mocks.tsx | 31 --- 49 files changed, 9 insertions(+), 2394 deletions(-) delete mode 100644 packages/fxa-content-server/app/scripts/lib/experiments/grouping-rules/push.js delete mode 100644 packages/fxa-content-server/app/scripts/templates/push/completed.mustache delete mode 100644 packages/fxa-content-server/app/scripts/templates/push/confirm_login.mustache delete mode 100644 packages/fxa-content-server/app/scripts/templates/push/send_login.mustache delete mode 100644 packages/fxa-content-server/app/scripts/views/mixins/push-login-experiment-mixin.js delete mode 100644 packages/fxa-content-server/app/scripts/views/push/completed.js delete mode 100644 packages/fxa-content-server/app/scripts/views/push/confirm_login.js delete mode 100644 packages/fxa-content-server/app/scripts/views/push/send_login.js delete mode 100644 packages/fxa-content-server/app/tests/spec/lib/experiments/grouping-rules/push.js delete mode 100644 packages/fxa-content-server/app/tests/spec/views/push/completed.js delete mode 100644 packages/fxa-content-server/app/tests/spec/views/push/confirm_login.js delete mode 100644 packages/fxa-content-server/app/tests/spec/views/push/send_login.js delete mode 100644 packages/fxa-settings/src/components/images/graphic_push_factor_auth.svg delete mode 100644 packages/fxa-settings/src/models/pages/signin/push-signin-query-params.ts delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/container.test.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/container.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/en.ftl delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/index.stories.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/index.test.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/index.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/interfaces.ts delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCode/mocks.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.test.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/en.ftl delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/greencheck.svg delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.stories.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx delete mode 100644 packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/mocks.tsx diff --git a/packages/fxa-auth-client/lib/client.ts b/packages/fxa-auth-client/lib/client.ts index a56b0a55f12..b1b65289dba 100644 --- a/packages/fxa-auth-client/lib/client.ts +++ b/packages/fxa-auth-client/lib/client.ts @@ -2625,35 +2625,6 @@ export default class AuthClient { ); } - async sendLoginPushRequest( - sessionToken: hexstring, - headers?: Headers - ): Promise { - return this.sessionPost( - '/session/verify/send_push', - sessionToken, - {}, - headers - ); - } - - async verifyLoginPushRequest( - sessionToken: hexstring, - tokenVerificationId: string, - code: string, - headers?: Headers - ): Promise { - return this.sessionPost( - '/session/verify/verify_push', - sessionToken, - { - tokenVerificationId, - code, - }, - headers - ); - } - async verifyTotpCode( sessionToken: hexstring, code: string, @@ -3144,15 +3115,6 @@ export default class AuthClient { ); } - async sendPushLoginRequest(sessionToken: string, headers?: Headers) { - return this.sessionPost( - '/session/verify/send_push', - sessionToken, - {}, - headers - ); - } - /** * Tries to register a recovery phone number. Important, this must be used for inline_recovery flow! * diff --git a/packages/fxa-auth-server/config/index.ts b/packages/fxa-auth-server/config/index.ts index 3aa361d2fdd..c23fd3ad965 100644 --- a/packages/fxa-auth-server/config/index.ts +++ b/packages/fxa-auth-server/config/index.ts @@ -2928,7 +2928,6 @@ convictConf.set( 'https://support.mozilla.org/kb/secure-mozilla-account-two-step-authentication' ); convictConf.set('smtp.verificationUrl', `${baseUri}/verify_email`); -convictConf.set('smtp.pushVerificationUrl', `${baseUri}/push/confirm_login`); convictConf.set('smtp.passwordResetUrl', `${baseUri}/complete_reset_password`); convictConf.set('smtp.initiatePasswordResetUrl', `${baseUri}/reset_password`); convictConf.set( diff --git a/packages/fxa-auth-server/docs/swagger/session-api.ts b/packages/fxa-auth-server/docs/swagger/session-api.ts index 60a2cfdc841..3f452b110a8 100644 --- a/packages/fxa-auth-server/docs/swagger/session-api.ts +++ b/packages/fxa-auth-server/docs/swagger/session-api.ts @@ -130,30 +130,6 @@ const SESSION_RESEND_CODE_POST = { notes: ['🔒 Authenticated with session token'], }; -const SESSION_SEND_PUSH_POST = { - ...TAGS_SESSION, - description: '/session/verify/send_push', - notes: [ - dedent` - 🔒 Authenticated with session token - - Sends a push notification to all push enabled devices to verify current session. - `, - ], -}; - -const SESSION_VERIFY_PUSH_POST = { - ...TAGS_SESSION, - description: '/session/verify/verify_push', - notes: [ - dedent` - 🔒 Authenticated with session token - - Endpoint that accepts a code and tokenVerificationId to verify a session. - `, - ], -}; - const API_DOCS = { SESSION_DESTROY_POST, SESSION_DUPLICATE_POST, @@ -161,8 +137,6 @@ const API_DOCS = { SESSION_STATUS_GET, SESSION_RESEND_CODE_POST, SESSION_VERIFY_CODE_POST, - SESSION_SEND_PUSH_POST, - SESSION_VERIFY_PUSH_POST, }; export default API_DOCS; diff --git a/packages/fxa-auth-server/lib/l10n/server.ftl b/packages/fxa-auth-server/lib/l10n/server.ftl index f76a39ee262..f095973c6aa 100644 --- a/packages/fxa-auth-server/lib/l10n/server.ftl +++ b/packages/fxa-auth-server/lib/l10n/server.ftl @@ -1,8 +1,5 @@ ## Non-email strings -session-verify-send-push-title-2 = Logging in to your { -product-mozilla-account }? -session-verify-send-push-body-2 = Click here to confirm it’s you - # Message sent by SMS with limited character length, please test translation with the messaging segment calculator # https://twiliodeved.github.io/message-segment-calculator/ # Messages should be limited to one segment diff --git a/packages/fxa-auth-server/lib/push.js b/packages/fxa-auth-server/lib/push.js index daa9650041e..46723626ce7 100644 --- a/packages/fxa-auth-server/lib/push.js +++ b/packages/fxa-auth-server/lib/push.js @@ -17,7 +17,6 @@ const PUSH_COMMANDS = { PASSWORD_RESET: 'fxaccounts:password_reset', ACCOUNT_DESTROYED: 'fxaccounts:account_destroyed', COMMAND_RECEIVED: 'fxaccounts:command_received', - LOGIN_REQUEST: 'fxaccounts:verify_login', }; const PUSH_REASONS = new Set([ @@ -31,7 +30,6 @@ const PUSH_REASONS = new Set([ 'devicesNotify', 'accountDestroyed', 'commandReceived', - 'verifyLogin', ]); const PUSH_ERRORS = new Set([ @@ -347,25 +345,6 @@ module.exports = function (log, db, config, statsd) { }); }, - /** - * Notify a device to verify a login request. The push notification - * contains location, UA and verification code in the url. - * - * @param {String} uid - * @param {Device[]} devices - * @param {Object} data - * @promise - */ - notifyVerifyLoginRequest(uid, devices, data) { - return this.sendPush(uid, devices, 'verifyLogin', { - data: { - version: PUSH_PAYLOAD_SCHEMA_VERSION, - command: PUSH_COMMANDS.LOGIN_REQUEST, - data, - }, - }); - }, - /** * Send a push notification with or without data to a list of devices * diff --git a/packages/fxa-auth-server/lib/routes/session.js b/packages/fxa-auth-server/lib/routes/session.js index 55e12d83edb..7fbb2eaef13 100644 --- a/packages/fxa-auth-server/lib/routes/session.js +++ b/packages/fxa-auth-server/lib/routes/session.js @@ -9,9 +9,6 @@ const isA = require('joi'); const requestHelper = require('../routes/utils/request_helper'); const METRICS_CONTEXT_SCHEMA = require('../metrics/context').schema; const validators = require('./validators'); -const Localizer = require('../l10n').default; -const NodeRendererBindings = - require('../senders/renderer/bindings-node').default; const SESSION_DOCS = require('../../docs/swagger/session-api').default; const DESCRIPTION = require('../../docs/swagger/shared/descriptions').default; const HEX_STRING = validators.HEX_STRING; @@ -703,244 +700,6 @@ module.exports = function ( } } - return {}; - }, - }, - { - method: 'POST', - path: '/session/verify/send_push', - options: { - ...SESSION_DOCS.SESSION_SEND_PUSH_POST, - auth: { - strategy: 'sessionToken', - }, - }, - handler: async function (request) { - log.begin('Session.send_push', request); - - const sessionToken = request.auth.credentials; - const { uid, email, tokenVerificationId } = sessionToken; - - // Check to see if this account has a verified TOTP token. If so, then it should - // not be allowed to bypass TOTP requirement by sending a sign-in push notification. - try { - const result = await db.totpToken(sessionToken.uid); - - if (result && result.verified && result.enabled) { - return {}; - } - } catch (err) { - if (err.errno !== error.ERRNO.TOTP_TOKEN_NOT_FOUND) { - throw err; - } - } - - const allDevices = await db.devices(uid); - - const account = await db.account(sessionToken.uid); - const secret = account.primaryEmail.emailCode; - - const code = otpUtils.generateOtpCode(secret, otpOptions); - - // Filter devices that can accept the push notification. - const filteredDevices = allDevices.filter((d) => { - // Don't push to the current device - if (d.sessionTokenId === sessionToken.id) { - return false; - } - // Exclude expired devices - if (d.pushEndpointExpired === true) { - return false; - } - // Currently, we only support sending push notifications to Firefox Desktop - return d.type === 'desktop' && d.uaBrowser === 'Firefox'; - }); - - const confirmUrl = `${config.contentServer.url}/signin_push_code_confirm`; - - const localizer = new Localizer(new NodeRendererBindings()); - - // If/when we use .localizeStrings in other files, probably move where strings are - // maintained to separate file? - const titleFtlId = 'session-verify-send-push-title-2'; - const bodyFtlId = 'session-verify-send-push-body-2'; - - const ftlIdMsgs = [ - { - id: titleFtlId, - message: 'Logging in to your Mozilla account?', - }, - { - id: bodyFtlId, - message: 'Click here to confirm it’s you', - }, - ]; - const localizedStrings = await localizer.localizeStrings( - request.app.locale, - ftlIdMsgs - ); - - const options = { - title: localizedStrings[titleFtlId], - body: localizedStrings[bodyFtlId], - }; - - const { region, city, country } = request.app.geo; - const remoteMetaData = { - deviceName: sessionToken.deviceName, - deviceFamily: sessionToken.uaBrowser, - deviceOS: sessionToken.uaOS, - ipAddress: request.app.clientAddress, - city, - region, - country, - }; - const params = new URLSearchParams({ - tokenVerificationId, - code, - uid, - email, - remoteMetaData: encodeURIComponent(JSON.stringify(remoteMetaData)), - }); - const url = `${confirmUrl}?${params.toString()}`; - try { - await push.notifyVerifyLoginRequest(uid, filteredDevices, { - ...options, - url, - }); - } catch (err) { - log.error('Session.send_push', { - uid: uid, - error: err, - }); - } - - return {}; - }, - }, - { - method: 'POST', - path: '/session/verify/verify_push', - options: { - ...SESSION_DOCS.SESSION_VERIFY_CODE_POST, - auth: { - strategy: 'sessionToken', - }, - validate: { - payload: isA.object({ - code: validators.DIGITS, - tokenVerificationId: validators.hexString.length(32), - }), - }, - }, - handler: async function (request) { - log.begin('Session.verify_push', request); - const options = request.payload; - const sessionToken = request.auth.credentials; - const { uid, email } = sessionToken; - const { code, tokenVerificationId } = options; - - await customs.checkAuthenticated( - request, - uid, - email, - 'verifySessionCode' - ); - request.emitMetricsEvent('session.verify_push'); - - const device = await db.deviceFromTokenVerificationId( - uid, - tokenVerificationId - ); - - // If device is not found, this means the device has already been verified. - // Since the user can not take any additional action, it is safe to return - // a successful response. - if (!device) { - return {}; - } - - // Check to see if the otp code passed matches the expected value from - // using the account's' `emailCode` as the secret in the otp code generation. - const account = await db.account(uid); - const secret = account.primaryEmail.emailCode; - - const { valid: isValidCode } = otpUtils.verifyOtpCode( - code, - secret, - otpOptions, - 'session.verify_push' - ); - - if (!isValidCode) { - if (customs.v2Enabled()) { - await customs.checkAuthenticated( - request, - uid, - email, - 'verifySessionCodeFailed' - ); - } - throw error.invalidOrExpiredOtpCode(); - } - - await db.verifyTokens(tokenVerificationId, account); - - // We have a matching code! Let's verify session and send the - // corresponding email and emit metrics. - request.emitMetricsEvent('account.confirmed', { uid }); - glean.login.verifyCodeConfirmed(request, { uid }); - await signinUtils.cleanupReminders({ verified: true }, account); - const devices = await db.devices(uid); - push - .notifyAccountUpdated(uid, devices, 'accountConfirm') - .catch((err) => log.error('push.accountConfirm.error', { uid, err })); - - // Send new device login notification email after successful verification - if (account.primaryEmail.isVerified) { - const geoData = request.app.geo; - const service = request.query.service; - const emailOptions = { - acceptLanguage: request.app.acceptLanguage, - ip: request.app.clientAddress, - location: geoData.location, - service, - timeZone: geoData.timeZone, - uaBrowser: sessionToken.uaBrowser, - uaBrowserVersion: sessionToken.uaBrowserVersion, - uaOS: sessionToken.uaOS, - uaOSVersion: sessionToken.uaOSVersion, - uaDeviceType: sessionToken.uaDeviceType, - uid, - }; - - try { - if (fxaMailer.canSend('newDeviceLogin')) { - const clientInfo = await oauthClientInfoService.fetch(service); - await fxaMailer.sendNewDeviceLoginEmail({ - ...FxaMailerFormat.account(account), - ...FxaMailerFormat.device(request), - ...FxaMailerFormat.localTime(request), - ...FxaMailerFormat.location(request), - ...(await FxaMailerFormat.metricsContext(request)), - ...FxaMailerFormat.sync(service), - clientName: clientInfo.name, - showBannerWarning: false, - }); - } else { - await mailer.sendNewDeviceLoginEmail( - account.emails, - account, - emailOptions - ); - } - } catch (err) { - log.trace('Session.verify_push.sendNewDeviceLoginEmail.error', { - error: err, - }); - } - } - return {}; }, }, diff --git a/packages/fxa-auth-server/lib/routes/session.spec.ts b/packages/fxa-auth-server/lib/routes/session.spec.ts index 99940acde5d..50d795db343 100644 --- a/packages/fxa-auth-server/lib/routes/session.spec.ts +++ b/packages/fxa-auth-server/lib/routes/session.spec.ts @@ -20,36 +20,6 @@ const signupCodeAccount = { tokenVerificationId: 'sometoken', }; -const MOCK_DEVICES = [ - // Current device - { - sessionTokenId: 'sessionTokenId', - name: 'foo', - type: 'desktop', - pushEndpointExpired: false, - pushPublicKey: 'foo', - uaBrowser: 'Firefox', - }, - // Only pushable device - { - sessionTokenId: 'sessionTokenId2', - name: 'foo2', - type: 'desktop', - pushEndpointExpired: false, - pushPublicKey: 'foo', - uaBrowser: 'Firefox', - }, - // Unsupported mobile device - { - sessionTokenId: 'sessionTokenId3', - name: 'foo3', - type: 'mobile', - pushEndpointExpired: false, - pushPublicKey: 'foo', - uaBrowser: 'Firefox', - }, -]; - function makeRoutes(options: any = {}) { const config = options.config || {}; config.oauth = config.oauth || {}; @@ -1236,206 +1206,3 @@ describe('/session/resend_code', () => { expect(args[0].code).toBe(expectedCode); }); }); - -describe('/session/verify/send_push', () => { - let route: any, request: any, log: any, db: any, mailer: any, push: any; - - beforeEach(() => { - db = mocks.mockDB({ ...signupCodeAccount, devices: MOCK_DEVICES }); - db.totpToken = jest.fn(() => Promise.resolve({ enabled: false })); - log = mocks.mockLog(); - mailer = mocks.mockMailer(); - push = mocks.mockPush(); - const config = { - contentServer: { url: 'http://localhost:3030' }, - }; - const routes = makeRoutes({ log, config, db, mailer, push }); - route = getRoute(routes, '/session/verify/send_push'); - - request = mocks.mockRequest({ - credentials: { - ...signupCodeAccount, - uaBrowser: 'Firefox', - id: 'sessionTokenId', - }, - log, - uaBrowser: 'Firefox', - }); - }); - - it('should send a push notification with verification code', async () => { - const response = await runTest(route, request); - expect(response).toEqual({}); - expect(db.devices).toHaveBeenCalledTimes(1); - expect(db.totpToken).toHaveBeenCalledTimes(1); - expect(db.account).toHaveBeenCalledTimes(1); - - const args = push.notifyVerifyLoginRequest.mock.calls[0]; - expect(args[0]).toBe('foo'); - expect(args[1]).toEqual([ - { - sessionTokenId: 'sessionTokenId2', - name: 'foo2', - type: 'desktop', - pushEndpointExpired: false, - pushPublicKey: 'foo', - uaBrowser: 'Firefox', - }, - ]); - expect(args[2].title).toBe('Logging in to your Mozilla account?'); - expect(args[2].body).toBe('Click here to confirm it\u2019s you'); - const url = args[2].url; - expect(url).toContain('http://localhost:3030/signin_push_code_confirm?'); - expect(url).toContain('tokenVerificationId=sometoken'); - expect(url).toMatch(/code=\d{6}/); - expect(url).toContain('uid=foo'); - expect(url).toContain('email=foo%40example.org'); - expect(url).toContain( - 'remoteMetaData=%257B%2522deviceFamily%2522%253A%2522Firefox%2522%252C%2522ipAddress%2522%253A%252263.245.221.32%2522%257D' - ); - }); - - it('should not send a push notification if TOTP token is verified and enabled', async () => { - db.totpToken = jest.fn(() => - Promise.resolve({ verified: true, enabled: true }) - ); - const response = await runTest(route, request); - expect(response).toEqual({}); - expect(db.totpToken).toHaveBeenCalledTimes(1); - expect(push.notifyVerifyLoginRequest).not.toHaveBeenCalled(); - }); -}); - -describe('/session/verify/verify_push', () => { - let route: any, - request: any, - log: any, - db: any, - mailer: any, - push: any, - customs: any; - - beforeEach(() => { - db = mocks.mockDB({ ...signupCodeAccount, devices: MOCK_DEVICES }); - db.deviceFromTokenVerificationId = jest.fn(() => - Promise.resolve(MOCK_DEVICES[1]) - ); - log = mocks.mockLog(); - mailer = mocks.mockMailer(); - push = mocks.mockPush(); - customs = mocks.mockCustoms(); - mocks.mockOAuthClientInfo(); - const config = {}; - const routes = makeRoutes({ log, config, db, mailer, push, customs }); - route = getRoute(routes, '/session/verify/verify_push'); - }); - - it('should verify push notification login request', async () => { - const expectedCode = getExpectedOtpCode({}, signupCodeAccount.emailCode); - request = mocks.mockRequest({ - log, - credentials: { - ...signupCodeAccount, - uaBrowser: 'Firefox', - id: 'sessionTokenId', - }, - payload: { - code: expectedCode, - uid: 'foo', - email: 'a@aa.com', - tokenVerificationId: 'sometoken', - }, - }); - const response = await runTest(route, request); - expect(response).toEqual({}); - - expect(customs.checkAuthenticated).toHaveBeenCalledTimes(1); - expect(customs.checkAuthenticated).toHaveBeenCalledWith( - request, - 'foo', - signupCodeAccount.email, - 'verifySessionCode' - ); - expect(db.devices).toHaveBeenCalledTimes(1); - expect(db.devices).toHaveBeenCalledWith('foo'); - expect(db.deviceFromTokenVerificationId).toHaveBeenCalledTimes(1); - expect(db.deviceFromTokenVerificationId).toHaveBeenCalledWith( - 'foo', - 'sometoken' - ); - expect(db.account).toHaveBeenCalledTimes(1); - expect(db.account).toHaveBeenNthCalledWith(1, 'foo'); - expect(db.verifyTokens).toHaveBeenCalledTimes(1); - expect(db.verifyTokens).toHaveBeenNthCalledWith( - 1, - 'sometoken', - expect.anything() - ); - - expect(push.notifyAccountUpdated).toHaveBeenCalledTimes(1); - expect(push.notifyAccountUpdated).toHaveBeenCalledWith( - 'foo', - MOCK_DEVICES, - 'accountConfirm' - ); - }); - - it('should return if session is already verified', async () => { - db.deviceFromTokenVerificationId = jest.fn(() => - Promise.resolve(undefined) - ); - request = mocks.mockRequest({ - log, - credentials: { - ...signupCodeAccount, - uaBrowser: 'Firefox', - id: 'sessionTokenId', - }, - payload: { - code: '123123', - uid: 'foo', - email: 'foo@example.org', - tokenVerificationId: 'sometoken', - }, - }); - const response = await runTest(route, request); - expect(response).toEqual({}); - expect(db.verifyTokens).not.toHaveBeenCalled(); - }); - - it('should fail if invalid code', async () => { - request = mocks.mockRequest({ - log, - credentials: { - ...signupCodeAccount, - uaBrowser: 'Firefox', - id: 'sessionTokenId', - }, - payload: { - code: '123123', - uid: 'foo', - email: 'foo@example.org', - tokenVerificationId: 'sometoken', - }, - }); - await expect(runTest(route, request)).rejects.toMatchObject({ - errno: 183, - message: 'Invalid or expired confirmation code', - }); - - expect(customs.checkAuthenticated).toHaveBeenCalledTimes(2); - expect(customs.checkAuthenticated).toHaveBeenCalledWith( - request, - 'foo', - 'foo@example.org', - 'verifySessionCode' - ); - - expect(customs.checkAuthenticated).toHaveBeenCalledWith( - request, - 'foo', - 'foo@example.org', - 'verifySessionCodeFailed' - ); - }); -}); diff --git a/packages/fxa-auth-server/test/mocks.js b/packages/fxa-auth-server/test/mocks.js index bca6e9bf994..e0675d12cde 100644 --- a/packages/fxa-auth-server/test/mocks.js +++ b/packages/fxa-auth-server/test/mocks.js @@ -220,7 +220,6 @@ const PUSH_METHOD_NAMES = [ 'notifyAccountDestroyed', 'notifyCommandReceived', 'notifyProfileUpdated', - 'notifyVerifyLoginRequest', 'sendPush', ]; diff --git a/packages/fxa-content-server/app/scripts/lib/experiment.js b/packages/fxa-content-server/app/scripts/lib/experiment.js index 9348020526d..3f993f2b1d3 100644 --- a/packages/fxa-content-server/app/scripts/lib/experiment.js +++ b/packages/fxa-content-server/app/scripts/lib/experiment.js @@ -25,7 +25,6 @@ const STARTUP_EXPERIMENTS = { const MANUAL_EXPERIMENTS = { emailMxValidation: BaseExperiment, qrCodeCad: BaseExperiment, - pushLogin: BaseExperiment, pocketMigration: BaseExperiment, }; diff --git a/packages/fxa-content-server/app/scripts/lib/experiments/grouping-rules/push.js b/packages/fxa-content-server/app/scripts/lib/experiments/grouping-rules/push.js deleted file mode 100644 index 1adda1c33df..00000000000 --- a/packages/fxa-content-server/app/scripts/lib/experiments/grouping-rules/push.js +++ /dev/null @@ -1,61 +0,0 @@ -/* 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/. */ - -/** - * This defines experiment groups the 2FA via push feature. Please reference - * https://docs.google.com/document/d/16fdARc6MC9XO7FBG9YJ4bpeqhHcv1G3ua0uzeS4EckY/edit# - * - */ -'use strict'; - -const BaseGroupingRule = require('./base'); -const GROUPS = [ - 'control', - // Treatment branches - 'treatment', -]; - -// This experiment is disabled by default. If you would like to go through -// the flow, load email-first screen and append query params -// `?forceExperiment=pushLogin&forceExperimentGroup=treatment` -const ROLLOUT_RATE = 0.0; - -module.exports = class PushLogin extends BaseGroupingRule { - constructor() { - super(); - this.name = 'pushLogin'; - - // Easier to set class properties for testability - this.groups = GROUPS; - this.rolloutRate = ROLLOUT_RATE; - } - - /** - * For this experiment, we are doing a staged rollout.` - * - * @param {Object} subject data used to decide - * @param {Boolean} isSync is this a sync signup? - * @returns {Any} - */ - choose(subject = {}) { - let choice = false; - const { isSync } = subject; - - // Only enroll for sync users - if (!isSync) { - return false; - } - - // TODO: Find out how Softvision plans to test and verify this in - // if(this.isTestEmail(subject.account.get('email'))) { - // return 'push'; - // } - - if (this.bernoulliTrial(this.rolloutRate, subject.uniqueUserId)) { - choice = this.uniformChoice(GROUPS, subject.uniqueUserId); - } - - return choice; - } -}; diff --git a/packages/fxa-content-server/app/scripts/lib/fxa-client.js b/packages/fxa-content-server/app/scripts/lib/fxa-client.js index cfe9aba52b4..de52753df85 100644 --- a/packages/fxa-content-server/app/scripts/lib/fxa-client.js +++ b/packages/fxa-content-server/app/scripts/lib/fxa-client.js @@ -1362,14 +1362,6 @@ FxaClientWrapper.prototype = { */ createCadReminder: createClientDelegate('createCadReminder'), - /** - * Sends a push notification to compatible devices that can verify a login - * request - * - * @returns {Promise} resolves with response when complete. - */ - sendPushLoginRequest: createClientDelegate('sendPushLoginRequest'), - finishSetup: withClient((client, relier, token, email, password) => { return client.finishSetup(token, email, password).then((accountData) => { return getUpdatedSessionData(email, relier, accountData); diff --git a/packages/fxa-content-server/app/scripts/lib/router.js b/packages/fxa-content-server/app/scripts/lib/router.js index 4c66ea0cd40..11d16e2a5b0 100644 --- a/packages/fxa-content-server/app/scripts/lib/router.js +++ b/packages/fxa-content-server/app/scripts/lib/router.js @@ -396,9 +396,6 @@ Router = Router.extend({ ); }, - 'push/confirm_login(/)': createViewHandler('push/confirm_login'), - 'push/send_login(/)': createViewHandler('push/send_login'), - 'push/completed(/)': createViewHandler('push/completed'), 'primary_email_verified(/)': function () { this.createReactOrBackboneViewHandler( 'primary_email_verified', @@ -580,20 +577,6 @@ Router = Router.extend({ } ); }, - 'signin_push_code(/)': function () { - this.createReactViewHandler('signin_push_code', { - ...Url.searchParams(this.window.location.search), - // for subplat redirect only - ...(this.relier.get('redirectTo') && { - redirect_to: this.relier.get('redirectTo'), - }), - }); - }, - 'signin_push_code_confirm(/)': function () { - this.createReactViewHandler('signin_push_code_confirm', { - ...Url.searchParams(this.window.location.search), - }); - }, 'signin_unblock(/)': function () { this.createReactOrBackboneViewHandler( 'signin_unblock', diff --git a/packages/fxa-content-server/app/scripts/models/account.js b/packages/fxa-content-server/app/scripts/models/account.js index 507b8d3a682..1b3a63a9b58 100644 --- a/packages/fxa-content-server/app/scripts/models/account.js +++ b/packages/fxa-content-server/app/scripts/models/account.js @@ -1704,14 +1704,6 @@ const Account = Backbone.Model.extend( return this._fxaClient.createCadReminder(this.get('sessionToken')); }, - /** - * Sends a push notification to verify a login request. - * - * @returns {Promise} resolves with response when complete. - */ - sendPushLoginRequest() { - return this._fxaClient.sendPushLoginRequest(this.get('sessionToken')); - }, }, { ALLOWED_KEYS: ALLOWED_KEYS, diff --git a/packages/fxa-content-server/app/scripts/templates/push/completed.mustache b/packages/fxa-content-server/app/scripts/templates/push/completed.mustache deleted file mode 100644 index a0f657568e8..00000000000 --- a/packages/fxa-content-server/app/scripts/templates/push/completed.mustache +++ /dev/null @@ -1,13 +0,0 @@ -
-
-

{{#t}}Sign-in confirmed{{/t}}

-
- -
-
- - - -

{{#t}}Please close this page and continue on the other device.{{/t}}

-
-
diff --git a/packages/fxa-content-server/app/scripts/templates/push/confirm_login.mustache b/packages/fxa-content-server/app/scripts/templates/push/confirm_login.mustache deleted file mode 100644 index 122b2a69023..00000000000 --- a/packages/fxa-content-server/app/scripts/templates/push/confirm_login.mustache +++ /dev/null @@ -1,25 +0,0 @@ -
-
-

{{#t}}New sign-in to Firefox{{/t}}

-
- -
-
-
- -
-

{{#t}}For added security, please confirm this sign-in to begin syncing with this device:{{/t}}

- - - -
- -
- -

{{#unsafeTranslate}}If you suspect that someone is trying to gain access to your account, please change your password.{{/unsafeTranslate}}

- -
-
-
diff --git a/packages/fxa-content-server/app/scripts/templates/push/send_login.mustache b/packages/fxa-content-server/app/scripts/templates/push/send_login.mustache deleted file mode 100644 index e5a03cbd922..00000000000 --- a/packages/fxa-content-server/app/scripts/templates/push/send_login.mustache +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/packages/fxa-content-server/app/scripts/views/mixins/push-login-experiment-mixin.js b/packages/fxa-content-server/app/scripts/views/mixins/push-login-experiment-mixin.js deleted file mode 100644 index ce8bada49cc..00000000000 --- a/packages/fxa-content-server/app/scripts/views/mixins/push-login-experiment-mixin.js +++ /dev/null @@ -1,18 +0,0 @@ -/* 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/. */ - -import ExperimentMixin from './experiment-mixin'; -const EXPERIMENT_NAME = 'pushLogin'; - -export default { - dependsOn: [ExperimentMixin], - - isInPushLoginExperiment() { - const experimentGroup = this.getAndReportExperimentGroup(EXPERIMENT_NAME, { - isSync: this.relier.isSync(), - }); - - return experimentGroup === 'treatment'; - }, -}; diff --git a/packages/fxa-content-server/app/scripts/views/mixins/signin-mixin.js b/packages/fxa-content-server/app/scripts/views/mixins/signin-mixin.js index 10c765efb12..0abc76fc9ee 100644 --- a/packages/fxa-content-server/app/scripts/views/mixins/signin-mixin.js +++ b/packages/fxa-content-server/app/scripts/views/mixins/signin-mixin.js @@ -11,10 +11,9 @@ import NavigateBehavior from '../behaviors/navigate'; import ResumeTokenMixin from './resume-token-mixin'; import VerificationMethods from '../../lib/verification-methods'; import VerificationReasons from '../../lib/verification-reasons'; -import PushLoginExperiment from './push-login-experiment-mixin'; export default { - dependsOn: [ResumeTokenMixin, PushLoginExperiment], + dependsOn: [ResumeTokenMixin], /** * Sign in a user @@ -188,10 +187,6 @@ export default { (verificationReason === VerificationReasons.CHANGE_PASSWORD && verificationMethod === VerificationMethods.EMAIL_OTP) ) { - if (this.isInPushLoginExperiment()) { - return this.navigate('/push/send_login'); - } - return this.navigate('signin_token_code', { account }); } diff --git a/packages/fxa-content-server/app/scripts/views/push/completed.js b/packages/fxa-content-server/app/scripts/views/push/completed.js deleted file mode 100644 index 3a949c0a72c..00000000000 --- a/packages/fxa-content-server/app/scripts/views/push/completed.js +++ /dev/null @@ -1,22 +0,0 @@ -/* 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/. */ - -import FormView from '../form'; -import Template from '../../templates/push/completed.mustache'; - -class CompletedPushLoginView extends FormView { - template = Template; - - beforeRender() { - const account = this.getSignedInAccount(); - // If no user is logged in redirect to the login page and set the `redirectTo` property - // to current url. After a user has logged in, they will be redirected back to this page. - if (account && account.isDefault()) { - this.relier.set('redirectTo', this.window.location.href); - return this.navigate('/'); - } - } -} - -export default CompletedPushLoginView; diff --git a/packages/fxa-content-server/app/scripts/views/push/confirm_login.js b/packages/fxa-content-server/app/scripts/views/push/confirm_login.js deleted file mode 100644 index 93eee1bda12..00000000000 --- a/packages/fxa-content-server/app/scripts/views/push/confirm_login.js +++ /dev/null @@ -1,59 +0,0 @@ -/* 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/. */ - -import { assign } from 'underscore'; -import FormView from '../form'; -import preventDefaultThen from '../decorators/prevent_default_then'; -import Template from '../../templates/push/confirm_login.mustache'; -import DeviceBeingPairedTemplate from '../../templates/partial/device-being-paired.mustache'; -import Url from '../../lib/url'; - -class ConfirmPushLoginView extends FormView { - template = Template; - - events = assign(this.events, { - 'click #change-password': preventDefaultThen('changePassword'), - }); - - initialize(options = {}) { - const params = Url.searchParams(this.window.location.search); - const ua = options.ua || JSON.parse(params.ua); - - const location = params.location ? JSON.parse(params.location) : {}; - const ip = options.ip || params.ip; - this.code = options.code || params.code; - - this.deviceContext = { - family: ua.uaBrowser, - OS: ua.uaOS, - ipAddress: ip, - ...location, - }; - } - - setInitialContext(context) { - context.set({ - unsafeDeviceBeingPairedHTML: this.renderTemplate( - DeviceBeingPairedTemplate, - this.deviceContext - ), - }); - } - - changePassword() {} - - submit() { - const account = this.getSignedInAccount(); - return account - .verifySignUp(this.code) - .then(() => { - return this.navigate('/push/completed'); - }) - .catch((err) => { - this.displayError(err); - }); - } -} - -export default ConfirmPushLoginView; diff --git a/packages/fxa-content-server/app/scripts/views/push/send_login.js b/packages/fxa-content-server/app/scripts/views/push/send_login.js deleted file mode 100644 index 01e4568b7bc..00000000000 --- a/packages/fxa-content-server/app/scripts/views/push/send_login.js +++ /dev/null @@ -1,77 +0,0 @@ -/* 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/. */ - -import { assign } from 'underscore'; -import FormView from '../form'; -import preventDefaultThen from '../decorators/prevent_default_then'; -import Template from '../../templates/push/send_login.mustache'; -import SessionVerificationPollMixin from '../mixins/session-verification-poll-mixin'; -import Cocktail from '../../lib/cocktail'; -import FlowEventsMixin from '../mixins/flow-events-mixin'; - -const proto = FormView.prototype; - -class SendPushLoginView extends FormView { - template = Template; - - events = assign(this.events, { - 'click #resend': preventDefaultThen('resend'), - 'click #send-email': preventDefaultThen('useEmailCode'), - }); - - beforeRender() { - const account = this.getSignedInAccount(); - return account - .sendPushLoginRequest() - .then(() => this.invokeBrokerMethod('beforeSignIn', account)); - } - - afterVisible() { - const account = this.getSignedInAccount(); - return proto.afterVisible - .call(this) - .then(() => this.broker.persistVerificationData(account)) - .then(() => - this.invokeBrokerMethod('beforeSignUpConfirmationPoll', account) - ) - .then(() => { - return this.waitForSessionVerification(account, () => { - this.logViewEvent('verification.success'); - this.notifier.trigger('verification.success'); - - return this.invokeBrokerMethod( - 'afterCompleteSignInWithCode', - account - ); - }); - }); - } - - resend() { - const account = this.getSignedInAccount(); - return account - .sendPushLoginRequest() - .then(() => { - this.displaySuccess('Notification sent'); - }) - .catch(() => { - this.displayError('Something went wrong'); - }); - } - - useEmailCode() { - const account = this.getSignedInAccount(); - return account.verifySessionResendCode().then(() => { - return this.navigate('/signin_token_code'); - }); - } -} - -Cocktail.mixin( - SendPushLoginView, - FlowEventsMixin, - SessionVerificationPollMixin -); - -export default SendPushLoginView; diff --git a/packages/fxa-content-server/app/tests/spec/lib/experiment.js b/packages/fxa-content-server/app/tests/spec/lib/experiment.js index 7c0eed09b70..3a9526eac47 100644 --- a/packages/fxa-content-server/app/tests/spec/lib/experiment.js +++ b/packages/fxa-content-server/app/tests/spec/lib/experiment.js @@ -89,9 +89,15 @@ describe('lib/experiment', () => { describe('createExperiment', () => { it('creates an experiment, only once, notifies of flow.initialize', () => { - const firstExperiment = expInt.createExperiment('pushLogin', 'treatment'); + const firstExperiment = expInt.createExperiment( + 'emailMxValidation', + 'treatment' + ); assert.ok(firstExperiment); - const secondExperiment = expInt.createExperiment('pushLogin', 'treatment'); + const secondExperiment = expInt.createExperiment( + 'emailMxValidation', + 'treatment' + ); // It's the same object, not updated assert.strictEqual(firstExperiment, secondExperiment); assert.isTrue(notifier.trigger.calledOnceWith('flow.initialize')); diff --git a/packages/fxa-content-server/app/tests/spec/lib/experiments/grouping-rules/push.js b/packages/fxa-content-server/app/tests/spec/lib/experiments/grouping-rules/push.js deleted file mode 100644 index 171e42a434e..00000000000 --- a/packages/fxa-content-server/app/tests/spec/lib/experiments/grouping-rules/push.js +++ /dev/null @@ -1,50 +0,0 @@ -/* 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/. */ - -import { assert } from 'chai'; -import Experiment from 'lib/experiments/grouping-rules/push'; - -describe('lib/experiments/grouping-rules/push', () => { - let experiment; - - beforeEach(() => { - experiment = new Experiment(); - }); - - describe('choose', () => { - it('returns false if not Sync', () => { - assert.isFalse( - experiment.choose({ - experimentGroupingRules: { choose: () => experiment.name }, - isSync: false, - uniqueUserId: 'user-id', - }) - ); - }); - - it('returns false if rollout 0%', () => { - experiment.rolloutRate = 0; - assert.isFalse( - experiment.choose({ - experimentGroupingRules: { choose: () => experiment.name }, - isSync: true, - uniqueUserId: 'user-id', - }) - ); - }); - - it('returns treatment if rollout 100%', () => { - experiment.rolloutRate = 1; - assert.isTrue( - experiment.groups.includes( - experiment.choose({ - experimentGroupingRules: { choose: () => experiment.name }, - isSync: true, - uniqueUserId: 'user-id', - }) - ) - ); - }); - }); -}); diff --git a/packages/fxa-content-server/app/tests/spec/views/mixins/signin-mixin.js b/packages/fxa-content-server/app/tests/spec/views/mixins/signin-mixin.js index a9922f72057..6d5b8eb5851 100644 --- a/packages/fxa-content-server/app/tests/spec/views/mixins/signin-mixin.js +++ b/packages/fxa-content-server/app/tests/spec/views/mixins/signin-mixin.js @@ -88,7 +88,6 @@ describe('views/mixins/signin-mixin', function () { signIn: SignInMixin.signIn, unsafeDisplayError: sinon.spy(), user: user, - isInPushLoginExperiment: sinon.spy(), isInReactExperiment: () => false, }; diff --git a/packages/fxa-content-server/app/tests/spec/views/push/completed.js b/packages/fxa-content-server/app/tests/spec/views/push/completed.js deleted file mode 100644 index f23dface5c6..00000000000 --- a/packages/fxa-content-server/app/tests/spec/views/push/completed.js +++ /dev/null @@ -1,76 +0,0 @@ -/* 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/. */ - -import { assert } from 'chai'; -import $ from 'jquery'; -import Account from 'models/account'; -import Backbone from 'backbone'; -import Metrics from 'lib/metrics'; -import Relier from 'models/reliers/relier'; -import View from 'views/push/completed'; -import _ from 'underscore'; -import sinon from 'sinon'; - -describe('views/push/completed', () => { - let account; - let model; - let relier; - let view; - let notifier; - let metrics; - - beforeEach(() => { - account = new Account({ - email: 'a@a.com', - uid: 'uid', - }); - - relier = new Relier({}); - model = new Backbone.Model({ - account, - }); - - notifier = _.extend({}, Backbone.Events); - metrics = new Metrics({ notifier }); - - view = new View({ - model, - relier, - notifier, - metrics, - }); - - sinon.stub(view, 'getSignedInAccount').callsFake(() => account); - - return view.render().then(() => $('#container').html(view.$el)); - }); - - afterEach(() => { - view.remove(); - view.destroy(); - view = null; - }); - - describe('render', () => { - it('renders the view', () => { - assert.lengthOf(view.$('#push-auth-complete-header'), 1); - assert.include( - view.$('.verification-message').text(), - 'Please close this page' - ); - }); - - describe('without an account', () => { - beforeEach(() => { - account = new Account({}); - sinon.spy(view, 'navigate'); - return view.render(); - }); - - it('redirects to the email first page', () => { - assert.isTrue(view.navigate.calledWith('/')); - }); - }); - }); -}); diff --git a/packages/fxa-content-server/app/tests/spec/views/push/confirm_login.js b/packages/fxa-content-server/app/tests/spec/views/push/confirm_login.js deleted file mode 100644 index 00191c2c17b..00000000000 --- a/packages/fxa-content-server/app/tests/spec/views/push/confirm_login.js +++ /dev/null @@ -1,123 +0,0 @@ -/* 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/. */ - -import _ from 'underscore'; -import { assert } from 'chai'; -import Account from 'models/account'; -import Backbone from 'backbone'; -import BaseBroker from 'models/auth_brokers/base'; -import Metrics from 'lib/metrics'; -import Relier from 'models/reliers/relier'; -import sinon from 'sinon'; -import User from 'models/user'; -import View from 'views/push/confirm_login'; -import WindowMock from '../../../mocks/window'; -import $ from 'jquery'; - -describe('views/push/confirm_login', () => { - let account; - let broker; - let metrics; - let model; - let notifier; - let relier; - let user; - let view; - let windowMock; - const ua = { - uaBrowser: 'Firefox', - uaOS: 'OSX', - }; - - beforeEach(() => { - windowMock = new WindowMock(); - - relier = new Relier({ - window: windowMock, - }); - - broker = new BaseBroker({ - relier: relier, - window: windowMock, - }); - - account = new Account({ - email: 'a@a.com', - uid: 'uid', - }); - - model = new Backbone.Model({ - account, - }); - - notifier = _.extend({}, Backbone.Events); - metrics = new Metrics({ notifier }); - - user = new User(); - - view = new View({ - broker, - metrics, - model, - notifier, - relier, - user, - window: windowMock, - ua, - ip: '123.123.123.123', - code: 'validCode', - }); - - sinon.stub(view, 'getSignedInAccount').callsFake(() => account); - - return view.render().then(() => $('#container').html(view.$el)); - }); - - afterEach(() => { - metrics.destroy(); - view.remove(); - view.destroy(); - view = metrics = null; - }); - - describe('render', () => { - it('renders the view', () => { - assert.lengthOf(view.$('#fxa-push-confirm-login-header'), 1); - assert.include( - view.$('.verification-message').text(), - 'please confirm this sign-in' - ); - assert.lengthOf(view.$('#submit-btn'), 1); - assert.lengthOf(view.$('#change-password'), 1); - assert.include(view.$('.push-confirm-login-device').text(), 'Firefox'); - assert.include( - view.$('.push-confirm-login-device').text(), - '123.123.123.123' - ); - assert.include( - view.$('.push-confirm-login-device').text(), - 'Location unknown' - ); - }); - }); - - describe('submit', () => { - describe('success', () => { - beforeEach(() => { - sinon.stub(account, 'verifySignUp').callsFake(() => Promise.resolve()); - sinon.spy(view, 'navigate'); - - return view.submit(); - }); - - it('calls correct methods', () => { - assert.isTrue( - account.verifySignUp.calledWith('validCode'), - 'verify with correct code' - ); - assert.isTrue(view.navigate.calledOnceWith('/push/completed')); - }); - }); - }); -}); diff --git a/packages/fxa-content-server/app/tests/spec/views/push/send_login.js b/packages/fxa-content-server/app/tests/spec/views/push/send_login.js deleted file mode 100644 index 2ea3b80738f..00000000000 --- a/packages/fxa-content-server/app/tests/spec/views/push/send_login.js +++ /dev/null @@ -1,155 +0,0 @@ -/* 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/. */ - -import _ from 'underscore'; -import { assert } from 'chai'; -import Account from 'models/account'; -import Backbone from 'backbone'; -import BaseBroker from 'models/auth_brokers/base'; -import Metrics from 'lib/metrics'; -import Relier from 'models/reliers/relier'; -import sinon from 'sinon'; -import User from 'models/user'; -import View from 'views/push/send_login'; -import WindowMock from '../../../mocks/window'; -import SentryMetrics from 'lib/sentry'; -import SessionVerificationPoll from 'models/polls/session-verification'; - -describe('views/push/send_login', () => { - let account; - let broker; - let metrics; - let model; - let notifier; - let relier; - let user; - let view; - let windowMock; - let sentryMetrics; - let sessionVerificationPoll; - - beforeEach(async () => { - windowMock = new WindowMock(); - - relier = new Relier({ - window: windowMock, - }); - - broker = new BaseBroker({ - relier: relier, - window: windowMock, - }); - - account = new Account({ - email: 'a@a.com', - uid: 'uid', - }); - - model = new Backbone.Model({ - account: account, - lastPage: 'signin', - password: 'password', - }); - - notifier = _.extend({}, Backbone.Events); - sentryMetrics = new SentryMetrics(); - metrics = new Metrics({ notifier, sentryMetrics }); - - user = new User(); - - sessionVerificationPoll = new SessionVerificationPoll( - {}, - { - account, - pollIntervalInMS: 2, - window: windowMock, - } - ); - - view = new View({ - broker, - canGoBack: true, - metrics, - model, - notifier, - relier, - user, - viewName: 'send-login', - window: windowMock, - sessionVerificationPoll, - }); - - sinon.stub(view, 'getSignedInAccount').callsFake(() => account); - sinon - .stub(account, 'sendPushLoginRequest') - .callsFake(() => Promise.resolve()); - sinon - .stub(view, '_handleSessionVerificationPollErrors') - .callsFake(() => {}); - sinon.stub(sessionVerificationPoll, 'start').callsFake(() => {}); - - return view.render(); - }); - - afterEach(function () { - metrics.destroy(); - view.remove(); - view.destroy(); - view = metrics = null; - }); - - describe('render', () => { - it('renders the view', () => { - assert.lengthOf(view.$('#fxa-push-send-login-header'), 1); - assert.include( - view.$('.verification-message').text(), - 'Check your connected Firefox devices' - ); - assert.lengthOf(view.$('#resend'), 1); - assert.lengthOf(view.$('#send-email'), 1); - }); - - it('sends push notification to account', () => { - assert.isTrue(account.sendPushLoginRequest.calledOnce); - }); - }); - - describe('afterVisible', () => { - beforeEach(async () => { - sinon.spy(broker, 'persistVerificationData'); - sinon.spy(view, 'waitForSessionVerification'); - sinon.spy(view, 'invokeBrokerMethod'); - return view.afterVisible().then(() => { - // simulate account being verified - return sessionVerificationPoll.trigger('verified'); - }); - }); - - it('starts polling for session to be verified', () => { - assert.isTrue(broker.persistVerificationData.calledOnceWith(account)); - assert.isTrue(view.waitForSessionVerification.calledOnce); - assert.isTrue(view.invokeBrokerMethod.calledTwice); - const args = view.invokeBrokerMethod.args; - assert.equal(args[0][0], 'beforeSignUpConfirmationPoll'); - assert.equal(args[1][0], 'afterCompleteSignInWithCode'); - }); - }); - - describe('resend', () => { - describe('success', () => { - beforeEach(() => { - sinon.spy(view, 'displaySuccess'); - return view.render().then(() => { - account.sendPushLoginRequest.resetHistory(); - return view.resend(); - }); - }); - - it('calls correct methods', () => { - assert.isTrue(account.sendPushLoginRequest.calledOnce); - assert.isTrue(view.displaySuccess.calledOnce); - }); - }); - }); -}); diff --git a/packages/fxa-content-server/app/tests/test_start.js b/packages/fxa-content-server/app/tests/test_start.js index 4f62163bbe6..80592952f31 100644 --- a/packages/fxa-content-server/app/tests/test_start.js +++ b/packages/fxa-content-server/app/tests/test_start.js @@ -58,7 +58,6 @@ require('./spec/lib/experiments/grouping-rules/base'); require('./spec/lib/experiments/grouping-rules/communication-prefs'); require('./spec/lib/experiments/grouping-rules/index'); require('./spec/lib/experiments/grouping-rules/is-sampled-user'); -require('./spec/lib/experiments/grouping-rules/push'); require('./spec/lib/experiments/grouping-rules/sentry'); require('./spec/lib/fxa-client'); // lib/glean spec skipped in the in-browser mocha bundle. Under @mozilla/glean @@ -238,9 +237,6 @@ require('./spec/views/post_verify/secondary_email/add_secondary_email'); require('./spec/views/post_verify/secondary_email/confirm_secondary_email'); require('./spec/views/post_verify/third_party_auth/set_password'); require('./spec/views/progress_indicator'); -require('./spec/views/push/confirm_login'); -require('./spec/views/push/send_login'); -require('./spec/views/push/completed'); require('./spec/views/ready'); require('./spec/views/report_sign_in'); require('./spec/views/reset_password'); diff --git a/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js b/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js index 4ffcda8255d..19fd796751c 100644 --- a/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js +++ b/packages/fxa-content-server/server/lib/routes/react-app/content-server-routes.js @@ -52,9 +52,6 @@ const FRONTEND_ROUTES = [ 'post_verify/third_party_auth/set_password', 'primary_email_verified', 'signup_confirmed_sync', // React app only - 'push/completed', - 'push/confirm_login', - 'push/send_login', 'report_signin', 'reset_password', 'reset_password_confirmed', @@ -68,8 +65,6 @@ const FRONTEND_ROUTES = [ 'signin', 'signin_bounced', 'signin_token_code', - 'signin_push_code', - 'signin_push_code_confirm', 'signin_totp_code', 'signin_recovery_code', 'signin_recovery_choice', diff --git a/packages/fxa-content-server/server/lib/routes/react-app/index.js b/packages/fxa-content-server/server/lib/routes/react-app/index.js index d4e268019ac..abd69a1bf70 100644 --- a/packages/fxa-content-server/server/lib/routes/react-app/index.js +++ b/packages/fxa-content-server/server/lib/routes/react-app/index.js @@ -95,8 +95,6 @@ const getReactRouteGroups = (showReactApp, reactRoute) => { 'inline_totp_setup', 'inline_recovery_setup', 'inline_recovery_key_setup', - 'signin_push_code', - 'signin_push_code_confirm', 'signin_passwordless_code', 'oauth/signin_passwordless_code', ]), diff --git a/packages/fxa-settings/src/components/App/index.tsx b/packages/fxa-settings/src/components/App/index.tsx index 8b1c842141f..36a132686a2 100644 --- a/packages/fxa-settings/src/components/App/index.tsx +++ b/packages/fxa-settings/src/components/App/index.tsx @@ -127,12 +127,6 @@ const SigninTokenCodeContainer = lazy( const SigninTotpCodeContainer = lazy( () => import('../../pages/Signin/SigninTotpCode/container') ); -const SigninPushCodeContainer = lazy( - () => import('../../pages/Signin/SigninPushCode/container') -); -const SigninPushCodeConfirmContainer = lazy( - () => import('../../pages/Signin/SigninPushCodeConfirm/container') -); const SigninUnblockContainer = lazy( () => import('../../pages/Signin/SigninUnblock/container') ); @@ -720,14 +714,6 @@ const AuthAndAccountSetupRoutes = ({ path="/signin_totp_code/*" {...{ integration, serviceName, setCurrentSplitLayout }} /> - - \ No newline at end of file diff --git a/packages/fxa-settings/src/components/images/index.tsx b/packages/fxa-settings/src/components/images/index.tsx index 51f53310a1e..b7c5f66b37b 100644 --- a/packages/fxa-settings/src/components/images/index.tsx +++ b/packages/fxa-settings/src/components/images/index.tsx @@ -12,7 +12,6 @@ import { ReactComponent as HeartsBroken } from './graphic_hearts_broken.svg'; import { ReactComponent as HeartsVerified } from './graphic_hearts_verified.svg'; import { ReactComponent as BackupCodes } from './graphic_backup_codes.min.svg'; import { ReactComponent as TwoFactorAuth } from './graphic_two_factor_auth.svg'; -import { ReactComponent as PushFactorAuth } from './graphic_push_factor_auth.svg'; import { ReactComponent as Mail } from './graphic_mail.svg'; import { ReactComponent as Key } from './graphic_recovery_key.min.svg'; import { ReactComponent as Password } from './graphic_password.min.svg'; @@ -115,15 +114,6 @@ export const TwoFactorAuthImage = ({ className, ariaHidden }: ImageProps) => ( /> ); -export const PushAuthImage = ({ className, ariaHidden }: ImageProps) => ( - -); - export const MailImage = ({ className, ariaHidden }: ImageProps) => ( { - return { - ...jest.requireActual('../../../models'), - useAuthClient: () => { - return { - checkTotpTokenExists: jest - .fn() - .mockResolvedValue({ verified: mockHasTotpAuthClient }), - sessionStatus: jest.fn().mockResolvedValue({ - state: mockSessionStatus, - }), - sendLoginPushRequest: mockSendLoginPushRequest, - }; - }, - useSensitiveDataClient: jest.fn(), - }; -}); - -// Set this when testing location state -let mockLocationState = {}; -const mockLocation = () => { - return { - pathname: '/signin_push_code', - state: mockLocationState, - }; -}; -const mockNavigate = jest.fn(); -jest.mock('@reach/router', () => { - return { - __esModule: true, - ...jest.requireActual('@reach/router'), - useNavigate: () => mockNavigate, - useLocation: () => mockLocation(), - }; -}); - -let currentSigninPushCodeProps: SigninPushCodeProps | undefined; -let moduleMock: jest.SpyInstance; -function mockSigninPushCodeModule() { - currentSigninPushCodeProps = undefined; - moduleMock = jest - .spyOn(SigninPushCodeModule, 'default') - .mockImplementation((props: SigninPushCodeProps) => { - currentSigninPushCodeProps = props; - return
signin push code mock
; - }); -} - -function mockReactUtilsModule() { - jest.spyOn(ReactUtils, 'hardNavigate').mockImplementation(() => {}); -} - -// Set this when testing local storage -function mockCurrentAccount(storedAccount = { uid: '123' }) { - jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(storedAccount); -} - -function resetMockSensitiveDataClient() { - (useSensitiveDataClient as jest.Mock).mockImplementation( - () => mockSensitiveDataClient - ); - mockSensitiveDataClient.getDataType = jest.fn().mockReturnValue({ - keyFetchToken: MOCK_KEY_FETCH_TOKEN, - unwrapBKey: MOCK_UNWRAP_BKEY, - }); -} - -async function render() { - renderWithLocalizationProvider( - - - - ); -} - -describe('SigninPushCode container', () => { - beforeEach(() => { - applyDefaultMocks(); - }); - - describe('initial states', () => { - describe('email', () => { - it('can be set from router state', async () => { - mockLocationState = createMockSigninLocationState(); - render(); - await waitFor(() => { - expect(CacheModule.currentAccount).not.toHaveBeenCalled(); - }); - expect(currentSigninPushCodeProps?.signinState.email).toBe(MOCK_EMAIL); - expect(SigninPushCodeModule.default).toHaveBeenCalled(); - }); - it('router state takes precedence over local storage', async () => { - mockLocationState = createMockSigninLocationState(); - render(); - expect(CacheModule.currentAccount).not.toHaveBeenCalled(); - await waitFor(() => { - expect(currentSigninPushCodeProps?.signinState.email).toBe( - MOCK_EMAIL - ); - }); - expect(SigninPushCodeModule.default).toHaveBeenCalled(); - }); - it('is read from localStorage if email is not provided via router state', async () => { - mockLocationState = {}; - mockCurrentAccount(MOCK_STORED_ACCOUNT); - render(); - expect(CacheModule.currentAccount).toHaveBeenCalled(); - await waitFor(() => { - expect(currentSigninPushCodeProps?.signinState.email).toBe( - MOCK_STORED_ACCOUNT.email - ); - }); - expect(SigninPushCodeModule.default).toHaveBeenCalled(); - }); - it('is handled if not provided in location state or local storage', async () => { - mockLocationState = {}; - render(); - expect(CacheModule.currentAccount).toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith('/'); - expect(SigninPushCodeModule.default).not.toHaveBeenCalled(); - }); - }); - - describe('totp status', () => { - beforeEach(() => { - mockLocationState = createMockSigninLocationState(); - }); - - it('redirects to totp screen if user has totp enabled', async () => { - mockHasTotpAuthClient = true; - render(); - - await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/signin_totp_code', { - state: mockLocationState, - }); - }); - }); - - it('does not redirect with totp false', async () => { - mockHasTotpAuthClient = false; - render(); - - await waitFor(() => { - expect(mockNavigate).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe('render', () => { - beforeEach(() => { - moduleMock.mockRestore(); - }); - - it('sends push notification', async () => { - mockSessionStatus = 'false'; - mockLocationState = createMockSigninLocationState(); - render(); - - await waitFor(() => expect(mockSendLoginPushRequest).toHaveBeenCalled()); - }); - - it('navigates when session verified', async () => { - mockSessionStatus = 'verified'; - mockLocationState = createMockSigninLocationState(); - render(); - - await waitFor(() => - expect(ReactUtils.hardNavigate).toHaveBeenCalledWith( - '/pair?showSuccessMessage=true', - undefined, - undefined, - false - ) - ); - }); - }); -}); diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/container.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCode/container.tsx deleted file mode 100644 index 0771e3f5d2a..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/container.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* 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/. */ - -import { RouteComponentProps, useLocation } from '@reach/router'; -import SigninPushCode from '.'; -import { MozServices } from '../../../lib/types'; -import { getSigninState, handleNavigation } from '../utils'; -import { SigninLocationState } from '../interfaces'; -import { - Integration, - isWebIntegration, - useAuthClient, - useSensitiveDataClient, -} from '../../../models'; -import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks'; -import AppLayout from '../../../components/AppLayout'; -import OAuthDataError from '../../../components/OAuthDataError'; -import { useWebRedirect } from '../../../lib/hooks/useWebRedirect'; -import { useEffect, useState } from 'react'; -import { useNavigateWithQuery } from '../../../lib/hooks/useNavigateWithQuery'; -import { SensitiveData } from '../../../lib/sensitive-data-client'; - -export type SigninPushCodeContainerProps = { - integration: Integration; - serviceName: MozServices; -}; - -export const SigninPushCodeContainer = ({ - integration, - serviceName, -}: SigninPushCodeContainerProps & RouteComponentProps) => { - const authClient = useAuthClient(); - const navigateWithQuery = useNavigateWithQuery(); - const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler( - authClient, - integration - ); - // TODO: FXA-9177, consider using localStorage instead of location state - const location = useLocation() as ReturnType & { - state: SigninLocationState; - }; - const signinState = getSigninState(location.state); - const sensitiveDataClient = useSensitiveDataClient(); - const { unwrapBKey } = - sensitiveDataClient.getDataType(SensitiveData.Key.Auth) || {}; - - const webRedirectCheck = useWebRedirect(integration.data.redirectTo); - - const redirectTo = - isWebIntegration(integration) && webRedirectCheck?.isValid - ? integration.data.redirectTo - : ''; - - const [totpVerified, setTotpVerified] = useState(false); - useEffect(() => { - if (!signinState || !signinState.sessionToken) { - // case handled after the useEffect - return; - } - const getTotpStatus = async () => { - const { verified } = await authClient.checkTotpTokenExists( - signinState.sessionToken - ); - setTotpVerified(verified); - }; - getTotpStatus(); - }, [authClient, signinState]); - - if (oAuthDataError) { - return ; - } - - if (!signinState) { - navigateWithQuery('/'); - return ; - } - - // redirect if there is 2FA is set up for the account, - // but the session is not TOTP verified - if (totpVerified) { - navigateWithQuery('/signin_totp_code', { - state: signinState, - }); - return ; - } - - const onCodeVerified = async () => { - const navigationOptions = { - email: signinState.email, - signinData: { - ...signinState, - emailVerified: true, - sessionVerified: true, - }, - unwrapBKey, - integration, - finishOAuthFlowHandler, - queryParams: location.search, - redirectTo, - }; - await handleNavigation(navigationOptions); - }; - - const sendLoginPushNotification = async () => { - try { - const response = await authClient.sessionStatus(signinState.sessionToken); - if (response.state !== 'verified') { - await authClient.sendLoginPushRequest(signinState.sessionToken); - } - if (response.state === 'verified') { - await onCodeVerified(); - } - } catch (error) { - console.error('Error sending push notification:', error); - } - }; - - const pollSessionStatus = async () => { - try { - const response = await authClient.sessionStatus(signinState.sessionToken); - if (response.state === 'verified') { - await onCodeVerified(); - } - } catch (error) { - console.error('Error fetching session status:', error); - } - }; - - return ( - - ); -}; - -export default SigninPushCodeContainer; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/en.ftl b/packages/fxa-settings/src/pages/Signin/SigninPushCode/en.ftl deleted file mode 100644 index 2cb7eeec03c..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/en.ftl +++ /dev/null @@ -1,8 +0,0 @@ -## SigninPushCode page -## This page is used to send a push notification to the user's device for two-factor authentication (2FA). - -signin-push-code-heading-w-default-service = Verify this login to continue to account settings -signin-push-code-heading-w-custom-service = Verify this login to continue to { $serviceName } -signin-push-code-instruction = Please check your other devices and approve this login from your { -brand-firefox } browser. -signin-push-code-did-not-recieve = Didn’t receive the notification? -signin-push-code-send-email-link = Email code diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.stories.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.stories.tsx deleted file mode 100644 index a7b1209aab9..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import SigninPushCode from '.'; -import { Meta } from '@storybook/react'; -import { withLocalization } from 'fxa-react/lib/storybooks'; -import { Subject } from './mocks'; - -export default { - title: 'Pages/Signin/SigninPushCode', - component: SigninPushCode, - decorators: [withLocalization], -} as Meta; - -export const Default = () => ; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.test.tsx deleted file mode 100644 index 5ad8f5e78b4..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.test.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import { screen } from '@testing-library/react'; -import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; // import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils'; -// import { FluentBundle } from '@fluent/bundle'; -import { MozServices } from '../../../lib/types'; -import { Subject } from './mocks'; - -const mockLocation = () => { - return { - pathname: '/signin_push_cpde', - }; -}; - -jest.mock('@reach/router', () => ({ - ...jest.requireActual('@reach/router'), - navigate: jest.fn(), - useLocation: () => mockLocation(), -})); - -describe('Sign in with push notification code page', () => { - // TODO: enable l10n tests when they've been updated to handle embedded tags in ftl strings - // TODO: in FXA-6461 - // let bundle: FluentBundle; - // beforeAll(async () => { - // bundle = await getFtlBundle('settings'); - // }); - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - - it('renders as expected', () => { - renderWithLocalizationProvider(); - // testAllL10n(screen, bundle); - - const headingEl = screen.getByRole('heading', { level: 1 }); - expect(headingEl).toHaveTextContent( - 'Verify this login to continue to account settings' - ); - screen.getByText( - 'Please check your other devices and approve this login from your Firefox browser.' - ); - - screen.getByText('Didn’t receive the notification?'); - screen.getByRole('link', { name: 'Email code' }); - }); - - it('shows the relying party in the header when a service name is provided', () => { - renderWithLocalizationProvider( - - ); - const headingEl = screen.getByRole('heading', { level: 1 }); - expect(headingEl).toHaveTextContent( - 'Verify this login to continue to Mozilla VPN' - ); - }); -}); diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.tsx deleted file mode 100644 index dce4a92c45b..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* 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/. */ - -import React, { useEffect } from 'react'; -import { Link, RouteComponentProps, useLocation } from '@reach/router'; -import { FtlMsg } from 'fxa-react/lib/utils'; -import { PushAuthImage } from '../../../components/images'; -import CardHeader from '../../../components/CardHeader'; -import AppLayout from '../../../components/AppLayout'; - -import { SigninPushCodeProps } from './interfaces'; - -export const viewName = 'signin-push-code'; - -const POLL_INTERVAL = 2000; // Poll every 2 seconds -const MAX_POLL_TIME = 300000; // 5 minutes - -export const SigninPushCode = ({ - signinState, - serviceName, - sendLoginPushNotification, - pollSessionStatus, -}: SigninPushCodeProps & RouteComponentProps) => { - const location = useLocation(); - - useEffect(() => { - sendLoginPushNotification(); - - const intervalId = setInterval(pollSessionStatus, POLL_INTERVAL); - const timeoutId = setTimeout( - () => clearInterval(intervalId), - MAX_POLL_TIME - ); - - return () => { - clearInterval(intervalId); - clearTimeout(timeoutId); - }; - }); - - return ( - - - -
- -
- - -

- Please check your other devices and approve this login from your - Firefox browser. -

-
- -
- -

Didn’t receive the notification?

-
- - - Email code - - -
-
- ); -}; - -export default SigninPushCode; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/interfaces.ts b/packages/fxa-settings/src/pages/Signin/SigninPushCode/interfaces.ts deleted file mode 100644 index ed28cf7580b..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/interfaces.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* 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/. */ - -import { SigninLocationState } from '../interfaces'; -import { MozServices } from '../../../lib/types'; - -export type SigninPushCodeProps = { - signinState: SigninLocationState; - serviceName?: MozServices; - sendLoginPushNotification: () => void; - pollSessionStatus: () => void; -}; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCode/mocks.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCode/mocks.tsx deleted file mode 100644 index a5fd5e8c531..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCode/mocks.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import { LocationProvider } from '@reach/router'; -import { IntegrationType } from '../../../models'; -import { SigninPushCode } from '.'; -import { SigninPushCodeProps } from './interfaces'; -import { - MOCK_EMAIL, - MOCK_KEY_FETCH_TOKEN, - MOCK_SESSION_TOKEN, - MOCK_UID, - MOCK_UNWRAP_BKEY, -} from '../../mocks'; -import { MozServices } from '../../../lib/types'; -import VerificationMethods from '../../../constants/verification-methods'; -import VerificationReasons from '../../../constants/verification-reasons'; - -export const MOCK_LOCATION_STATE = { - email: MOCK_EMAIL, - uid: MOCK_UID, - sessionToken: MOCK_SESSION_TOKEN, - emailVerified: false, - sessionVerified: false, - verificationMethod: VerificationMethods.EMAIL_OTP, -}; - -export const createMockSigninLocationState = ( - wantsKeys = false, - verificationReason?: VerificationReasons -) => { - return { - email: MOCK_EMAIL, - uid: MOCK_UID, - sessionToken: MOCK_SESSION_TOKEN, - emailVerified: false, - sessionVerified: false, - verificationReason, - ...(wantsKeys && { - keyFetchToken: MOCK_KEY_FETCH_TOKEN, - unwrapBKey: MOCK_UNWRAP_BKEY, - }), - }; -}; - -export function createMockSyncIntegration() { - return { - type: IntegrationType.SyncDesktopV3, - getService: () => MozServices.FirefoxSync, - isSync: () => true, - requiresKeys: () => true, - wantsKeys: () => true, - getCmsInfo: () => undefined, - data: {}, - }; -} - -export const Subject = ({ - serviceName = MozServices.Default, - signinState = MOCK_LOCATION_STATE, -}: Partial) => { - return ( - - Promise.resolve(), - pollSessionStatus: () => Promise.resolve(), - }} - /> - - ); -}; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.test.tsx deleted file mode 100644 index 51cf43238e2..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* 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/. */ - -import * as ReactUtils from 'fxa-react/lib/utils'; -import { SigninPushCodeConfirmContainer } from './container'; - -import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; -import { LocationProvider } from '@reach/router'; - -import * as UseValidateModule from '../../../lib/hooks/useValidate'; -import { MOCK_HEXSTRING_32, MOCK_REMOTE_METADATA } from '../../mocks'; -import { ModelDataProvider } from '../../../lib/model-data'; -import { fireEvent, screen, waitFor } from '@testing-library/react'; - -function applyDefaultMocks() { - jest.resetAllMocks(); - jest.restoreAllMocks(); - - mockUseValidateModule(); - mockReactUtilsModule(); -} - -let mockVerifyLoginPushRequest = jest.fn().mockResolvedValue({}); -jest.mock('../../../models', () => { - return { - ...jest.requireActual('../../../models'), - useAuthClient: () => { - return { - verifyLoginPushRequest: mockVerifyLoginPushRequest, - }; - }, - }; -}); - -const mockNavigate = jest.fn(); -jest.mock('@reach/router', () => { - return { - __esModule: true, - ...jest.requireActual('@reach/router'), - useNavigate: () => mockNavigate, - }; -}); - -function mockUseValidateModule() { - jest.spyOn(UseValidateModule, 'useValidatedQueryParams').mockReturnValue({ - queryParamModel: { - code: '123456', - tokenVerificationId: MOCK_HEXSTRING_32, - remoteMetaData: MOCK_REMOTE_METADATA, - } as unknown as ModelDataProvider, - validationError: undefined, - }); -} - -function mockReactUtilsModule() { - jest.spyOn(ReactUtils, 'hardNavigate').mockImplementation(() => {}); -} - -async function render(options = {}) { - renderWithLocalizationProvider( - - - - ); -} - -describe('SigninPushCodeConfirm container', () => { - beforeEach(() => { - applyDefaultMocks(); - }); - - it('can verify push notification', async () => { - render(); - fireEvent.click(screen.getByText('Confirm login')); - await waitFor(() => { - expect(mockVerifyLoginPushRequest).toHaveBeenCalledWith( - null, - MOCK_HEXSTRING_32, - '123456' - ); - }); - screen.getByText('Your login has been approved. Please close this window.'); - }); -}); diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.tsx deleted file mode 100644 index 9ba7aa06589..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/container.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* 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/. */ - -import React, { useState } from 'react'; -import { RouteComponentProps } from '@reach/router'; -import { useAuthClient } from '../../../models'; -import SigninPushCodeConfirm from './index'; -import { useValidatedQueryParams } from '../../../lib/hooks/useValidate'; -import { PushSigninQueryParams } from '../../../models/pages/signin/push-signin-query-params'; -import { FtlMsg } from '../../../../../fxa-react/lib/utils'; -import { sessionToken } from '../../../lib/cache'; - -export const SigninPushCodeConfirmContainer = (props: RouteComponentProps) => { - const authClient = useAuthClient(); - const { queryParamModel, validationError } = useValidatedQueryParams( - PushSigninQueryParams - ); - const { tokenVerificationId, code, remoteMetaData } = queryParamModel; - const remoteMetaDataParsed = JSON.parse(decodeURIComponent(remoteMetaData)); - const [sessionVerified, setSessionVerified] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(''); - - if (validationError) { - return ( -
- -

Link is damaged. Please try again.

-
-
- ); - } - - const handleSubmit = async () => { - setIsLoading(true); - try { - await authClient.verifyLoginPushRequest( - sessionToken()!, - tokenVerificationId, - code - ); - setSessionVerified(true); - } catch (error) { - setErrorMessage('Error verifying login push request'); - } finally { - setIsLoading(false); - } - }; - - return ( - - ); -}; - -export default SigninPushCodeConfirmContainer; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/en.ftl b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/en.ftl deleted file mode 100644 index 2fc92c3e956..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/en.ftl +++ /dev/null @@ -1,9 +0,0 @@ -## SigninPushCodeConfirmPage - -signin-push-code-confirm-instruction = Confirm your login -signin-push-code-confirm-description = We detected a login attempt from the following device. If this was you, please approve the login -signin-push-code-confirm-verifying = Verifying -signin-push-code-confirm-login = Confirm login -signin-push-code-confirm-wasnt-me = This wasn’t me, change password. -signin-push-code-confirm-login-approved = Your login has been approved. Please close this window. -signin-push-code-confirm-link-error = Link is damaged. Please try again. \ No newline at end of file diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/greencheck.svg b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/greencheck.svg deleted file mode 100644 index 96d95e9f224..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/greencheck.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.stories.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.stories.tsx deleted file mode 100644 index a8498c1d8b9..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.stories.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import SigninPushCode from '.'; -import { Meta } from '@storybook/react'; -import { withLocalization } from 'fxa-react/lib/storybooks'; -import { Subject } from './mocks'; - -export default { - title: 'Pages/Signin/SigninPushCodeConfrim', - component: SigninPushCode, - decorators: [withLocalization], -} as Meta; - -export const Default = () => ; - -export const Verifying = () => ; - -export const SessionVerified = () => ; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx deleted file mode 100644 index 0ec49402b91..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import { Link, RouteComponentProps } from '@reach/router'; -import { RemoteMetadata } from '../../../lib/types'; - -import AppLayout from '../../../components/AppLayout'; -import DeviceInfoBlock from '../../../components/DeviceInfoBlock'; -import { FtlMsg } from '../../../../../fxa-react/lib/utils'; - -// Reuse these images temporarily -import monitorIcon from '../../../components/Settings/BentoMenu/monitor.svg'; -import relayIcon from '../../../components/Settings/BentoMenu/relay.svg'; -import vpnIcon from '../../../components/Settings/BentoMenu/vpn-logo.svg'; -import checkmarkIcon from './greencheck.svg'; -import { LinkExternal } from 'fxa-react/components/LinkExternal'; -import Banner from '../../../components/Banner'; - -export type SigninPushCodeConfirmProps = { - authDeviceInfo: RemoteMetadata; - handleSubmit: () => void; - sessionVerified: boolean; - isLoading: boolean; - errorMessage?: string; -}; - -const Products = () => { - const products = [ - { - icon: monitorIcon, - title: 'Mozilla Monitor', - description: - 'Get notified if your information is involved in a data breach.', - link: 'https://monitor.firefox.com/', - }, - { - icon: relayIcon, - title: 'Firefox Relay', - description: - 'Keep your email address safe from spam and unwanted emails.', - link: 'https://relay.firefox.com/', - }, - { - icon: vpnIcon, - title: 'Mozilla VPN', - description: - 'Protect your internet connection and browse privately with Mozilla VPN.', - link: 'https://vpn.mozilla.org/', - }, - ]; - - // Note this isn't localized yet since the UX will most likely change - return ( -
-

- Explore products from Mozilla that protect your privacy -

- {products.map((product, index) => ( -
- {`${product.title} -
-

{product.title}

-

- {product.description} - - {' '} - Learn more - -

-
-
- ))} -
- ); -}; - -const LoginApprovedMessage = () => { - return ( -
- Checkmark Icon - -

- Your login has been approved. Please close this window. -

-
- -
- ); -}; - -const SigninPushCodeConfirm = ({ - authDeviceInfo, - handleSubmit, - sessionVerified, - isLoading, - errorMessage, -}: SigninPushCodeConfirmProps & RouteComponentProps) => { - return ( - - {sessionVerified ? ( - - ) : ( - <> - -

Confirm your login

-
- - {errorMessage && ( - - )} - - -

- We detected a login attempt from the following device. If this was - you, please approve the login. -

-
- - -
- - - - - This wasn’t me, change password. - - -
- - )} -
- ); -}; - -export default SigninPushCodeConfirm; diff --git a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/mocks.tsx b/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/mocks.tsx deleted file mode 100644 index 09a0bc5632c..00000000000 --- a/packages/fxa-settings/src/pages/Signin/SigninPushCodeConfirm/mocks.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* 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/. */ - -import React from 'react'; -import SigninPushCodeConfirm, { SigninPushCodeConfirmProps } from '.'; - -export const Subject = ({ - authDeviceInfo, - sessionVerified, - isLoading, -}: Partial) => { - return ( - {}} - authDeviceInfo={ - authDeviceInfo || { - deviceName: 'MacBook Pro', - deviceFamily: 'Device Family', - deviceOS: 'Device OS', - ipAddress: '123.123.123.123', - city: 'City', - region: 'Region', - country: 'Country', - } - } - /> - ); -};