From bc51a5f792a7cfec38d0b71313a50e024dafe3ed Mon Sep 17 00:00:00 2001 From: Antonin Cezard Date: Wed, 21 Feb 2024 09:46:39 +0100 Subject: [PATCH] feat: bypass 2FA when logging with email Using a react ref to store the param, as to not disturb the existing flow. cozy-client has been updated to use the new loginFlagship method. Keep in mind this has not been deployed into prod stack-wise at the time of this commit. The feature will not work with only the front-end implementation here. --- package.json | 2 +- .../deeplinks/models/OnboardingParams.ts | 1 + .../services/DeeplinksParserService.spec.ts | 1 + .../services/DeeplinksParserService.ts | 14 +++++++--- src/hooks/useAppBootstrap.spec.js | 1 + src/libs/Launcher.spec.js | 3 ++- src/libs/clientHelpers/connectClient.ts | 8 ++++-- src/libs/clientHelpers/initClient.ts | 8 ++++-- src/libs/clientHelpers/loginFlagship.ts | 9 ++++--- src/screens/login/LoginScreen.js | 26 ++++++++++++++----- yarn.lock | 18 ++++++------- 11 files changed, 63 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index e29494a79..5259bf900 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@sentry/integrations": "7.81.1", "@sentry/react-native": "5.16.0", "base-64": "^1.0.0", - "cozy-client": "^45.12.0", + "cozy-client": "^45.13.0", "cozy-clisk": "^0.34.0", "cozy-device-helper": "^2.7.0", "cozy-flags": "^3.2.0", diff --git a/src/app/domain/deeplinks/models/OnboardingParams.ts b/src/app/domain/deeplinks/models/OnboardingParams.ts index 869fcbb34..52d82f44a 100644 --- a/src/app/domain/deeplinks/models/OnboardingParams.ts +++ b/src/app/domain/deeplinks/models/OnboardingParams.ts @@ -2,4 +2,5 @@ export interface OnboardingParams { onboardUrl: string | null onboardedRedirection: string | null fqdn: string | null + emailVerifiedCode: string | null } diff --git a/src/app/domain/deeplinks/services/DeeplinksParserService.spec.ts b/src/app/domain/deeplinks/services/DeeplinksParserService.spec.ts index 2d4824d6a..2f3160638 100644 --- a/src/app/domain/deeplinks/services/DeeplinksParserService.spec.ts +++ b/src/app/domain/deeplinks/services/DeeplinksParserService.spec.ts @@ -29,6 +29,7 @@ describe('DeeplinksParserService', () => { expect(result).toStrictEqual({ route: routes.authenticate, params: { + emailVerifiedCode: null, fqdn: 'SOME_FQDN' }, onboardedRedirection: null diff --git a/src/app/domain/deeplinks/services/DeeplinksParserService.ts b/src/app/domain/deeplinks/services/DeeplinksParserService.ts index 3552c6aaf..4d6f23067 100644 --- a/src/app/domain/deeplinks/services/DeeplinksParserService.ts +++ b/src/app/domain/deeplinks/services/DeeplinksParserService.ts @@ -61,15 +61,19 @@ export const parseOnboardingURL = ( 'onboarded_redirection' ) const fqdn = onboardingUrl.searchParams.get('fqdn') + const emailVerifiedCode = onboardingUrl.searchParams.get( + 'email_verified_code' + ) if (!onboardUrl && !fqdn) { return undefined } return { + emailVerifiedCode, + fqdn, onboardUrl, - onboardedRedirection, - fqdn + onboardedRedirection } } catch (error) { const errorMessage = getErrorMessage(error) @@ -183,7 +187,8 @@ export const parseOnboardLink = ( const { onboardUrl, onboardedRedirection: onboardedRedirectionParam, - fqdn + fqdn, + emailVerifiedCode } = onboardingParams if (onboardUrl) { @@ -200,7 +205,8 @@ export const parseOnboardLink = ( return { route: routes.authenticate, params: { - fqdn + fqdn, + emailVerifiedCode }, onboardedRedirection: onboardedRedirectionParam } diff --git a/src/hooks/useAppBootstrap.spec.js b/src/hooks/useAppBootstrap.spec.js index 06f7465be..e7bb70b8f 100644 --- a/src/hooks/useAppBootstrap.spec.js +++ b/src/hooks/useAppBootstrap.spec.js @@ -152,6 +152,7 @@ it('should set routes.stack and authenticate - when onboard_url not provided', a initialRoute: { route: routes.authenticate, params: { + emailVerifiedCode: null, fqdn: paramFqdn } }, diff --git a/src/libs/Launcher.spec.js b/src/libs/Launcher.spec.js index 84027d210..44f42e384 100644 --- a/src/libs/Launcher.spec.js +++ b/src/libs/Launcher.spec.js @@ -50,7 +50,8 @@ describe('Launcher', () => { getInstanceOptions: jest.fn().mockReturnValueOnce({ locale: 'fr' }), save: jest.fn().mockResolvedValueOnce({ data: { message: { folder_to_save: 'newfolderid' } } - }) + }), + query: jest.fn().mockResolvedValueOnce({ data: { path: 'folderPath' } }) } const konnector = {} diff --git a/src/libs/clientHelpers/connectClient.ts b/src/libs/clientHelpers/connectClient.ts index 7daf4eeb5..b95d83bf3 100644 --- a/src/libs/clientHelpers/connectClient.ts +++ b/src/libs/clientHelpers/connectClient.ts @@ -22,6 +22,7 @@ interface ConnectClientParams { loginData: LoginData client: CozyClient twoFactorAuthenticationData?: TwoFactorAuthenticationData + emailVerifiedCode?: string } /** @@ -31,17 +32,20 @@ interface ConnectClientParams { * @param {LoginData} param.loginData - login data containing hashed password and encryption keys * @param {CozyClient} param.client - the CozyClient instance that will be authenticated through OAuth * @param {TwoFactorAuthenticationData} param.twoFactorAuthenticationData - the 2FA data containing a token and a passcode + * @param {string} param.emailVerifiedCode - the emailVerifiedCode that should be used to log in the stack * @returns {CozyClientCreationContext} The CozyClient with its corresponding state (i.e: connected, waiting for 2FA, invalid password etc) */ export const connectClient = async ({ loginData, client, - twoFactorAuthenticationData = undefined + twoFactorAuthenticationData = undefined, + emailVerifiedCode = undefined }: ConnectClientParams): Promise => { const result = await loginFlagship({ client, loginData, - twoFactorAuthenticationData + twoFactorAuthenticationData, + emailVerifiedCode }) if (isInvalidPasswordResult(result)) { diff --git a/src/libs/clientHelpers/initClient.ts b/src/libs/clientHelpers/initClient.ts index fffcceab0..ab5dd1034 100644 --- a/src/libs/clientHelpers/initClient.ts +++ b/src/libs/clientHelpers/initClient.ts @@ -17,6 +17,7 @@ interface CallInitClientParams { client?: CozyClient instance: string loginData: LoginData + emailVerifiedCode?: string } /** @@ -26,18 +27,21 @@ interface CallInitClientParams { * @param {LoginData} param.loginData - login data containing hashed password and encryption keys * @param {string} param.instance - the Cozy instance used to create the client * @param {CozyClient} [param.client] - an optional CozyClient instance that can be used for the authentication. If not provided a new CozyClient will be created + * @param {string} [param.emailVerifiedCode] - the emailVerifiedCode that should be used to log in the stack * @returns {CozyClientCreationContext} The CozyClient for the Cozy instance with its corresponding state (i.e: connected, waiting for 2FA, invalid password etc) */ export const callInitClient = async ({ loginData, instance, - client: clientParam + client: clientParam, + emailVerifiedCode }: CallInitClientParams): Promise => { const client = clientParam ?? (await createClient(instance)) return await connectClient({ loginData, - client + client, + emailVerifiedCode }) } diff --git a/src/libs/clientHelpers/loginFlagship.ts b/src/libs/clientHelpers/loginFlagship.ts index 99436ee9a..3591c31d4 100644 --- a/src/libs/clientHelpers/loginFlagship.ts +++ b/src/libs/clientHelpers/loginFlagship.ts @@ -14,6 +14,7 @@ interface LoginFlagshipParams { client: CozyClient loginData: LoginData twoFactorAuthenticationData?: TwoFactorAuthenticationData + emailVerifiedCode?: string } /** @@ -25,16 +26,17 @@ interface LoginFlagshipParams { * @param {object} param.client - the CozyClient instance that will be authenticated through OAuth * @param {object} param.loginData - login data containing hashed password * @param {object} [param.twoFactorAuthenticationData] - the 2FA data containing a token and a passcode + * @param {string} [param.emailVerifiedCode] - the emailVerifiedCode that should be used to log in the stack * @returns {LoginFlagshipResult} The query result with session_code, or 2FA token, or invalid password error * @throws */ export const loginFlagship = async ({ client, loginData, - twoFactorAuthenticationData = undefined + twoFactorAuthenticationData = undefined, + emailVerifiedCode }: LoginFlagshipParams): Promise => { const stackClient = client.getStackClient() - try { const loginResult = await stackClient.loginFlagship({ passwordHash: loginData.passwordHash, @@ -43,7 +45,8 @@ export const loginFlagship = async ({ : undefined, twoFactorPasscode: twoFactorAuthenticationData ? twoFactorAuthenticationData.passcode - : undefined + : undefined, + ...(emailVerifiedCode ? { emailVerifiedCode } : {}) }) return loginResult diff --git a/src/screens/login/LoginScreen.js b/src/screens/login/LoginScreen.js index 2bc410a8c..3ecc6ac84 100644 --- a/src/screens/login/LoginScreen.js +++ b/src/screens/login/LoginScreen.js @@ -1,3 +1,4 @@ +import { changeLanguage } from 'i18next' import React, { useCallback, useEffect, useState } from 'react' import { BackHandler, StyleSheet, View } from 'react-native' @@ -36,7 +37,6 @@ import { routes } from '/constants/routes' import { setStatusBarColorToMatchBackground } from '/screens/login/components/functions/clouderyBackgroundFetcher' import { getInstanceFromFqdn } from '/screens/login/components/functions/getInstanceFromFqdn' import { getInstanceDataFromFqdn } from '/screens/login/components/functions/getInstanceDataFromRequest' -import { changeLanguage } from 'i18next' const log = Minilog('LoginScreen') @@ -107,6 +107,12 @@ const LoginSteps = ({ const magicCode = consumeRouteParameter('magicCode', route, navigation) const oauthCode = consumeRouteParameter('code', route, navigation) const onboardUrl = consumeRouteParameter('onboardUrl', route, navigation) + const emailVerifiedCode = consumeRouteParameter( + 'emailVerifiedCode', + route, + navigation + ) + if (fqdn) { const instanceData = getInstanceDataFromFqdn(fqdn) @@ -126,7 +132,7 @@ const LoginSteps = ({ } else if (oauthCode) { startOidcOAuth(instanceData.fqdn, oauthCode) } else { - setInstanceData(instanceData) + setInstanceData(instanceData, { emailVerifiedCode }) } } else if (onboardUrl && oauthCode) { // when receiving fqdn from route parameter, we don't have access to partner context @@ -158,7 +164,13 @@ const LoginSteps = ({ } const setInstanceData = useCallback( - async ({ instance, fqdn }) => { + /** + * Sets the instance data. + * @param {{ instance: Record, fqdn: string }} instanceData - The first parameter object with `instance` as a record of string keys to unknown values, and `fqdn` as a string. + * @param {{ emailVerifiedCode?: string }} [options] - The optional second parameter object with an optional `emailVerifiedCode` property. + * @returns {Promise} A promise that resolves to void. + */ + async ({ instance, fqdn }, { emailVerifiedCode }) => { if (await NetService.isOffline()) NetService.handleOffline(routes.authenticate) @@ -183,7 +195,8 @@ const LoginSteps = ({ instance: instance, name: name, kdfIterations: kdfIterations, - client: client + client: client, + emailVerifiedCode }) } catch (error) { setError(error.message, error) @@ -332,12 +345,13 @@ const LoginSteps = ({ NetService.handleOffline(routes.authenticate) try { - const { loginData, instance, client } = state + const { loginData, instance, client, emailVerifiedCode } = state const result = await callInitClient({ loginData, instance, - client + client, + emailVerifiedCode }) if (result.state === STATE_INVALID_PASSWORD) { diff --git a/yarn.lock b/yarn.lock index 62ce87cf2..4c0d13a91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7943,16 +7943,16 @@ cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cozy-client@^45.12.0: - version "45.12.0" - resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-45.12.0.tgz#e550c86a5b5fc430fec71ebba43c7ba1cd58c0db" - integrity sha512-Vic8k6f6LKY1AJt0rMLe1AHX51kB3ROYsXnpj7JduUa3Tise1Y/9iCQfWmWMVdSicFtmEeZXqt0Z/ot3gt0W4A== +cozy-client@^45.13.0: + version "45.13.0" + resolved "https://registry.yarnpkg.com/cozy-client/-/cozy-client-45.13.0.tgz#4e9eda7ac30312ccc7de99c9525e62d61e0a08d8" + integrity sha512-bftRIKkjfdjGtWGguvhuuohWElUSN9q/x+u6ksCx6iiM7VWpPBIJEMqfj+tA+RkghJnN80Phdguk3HMkENxqEA== dependencies: "@cozy/minilog" "1.0.0" "@types/jest" "^26.0.20" "@types/lodash" "^4.14.170" btoa "^1.2.1" - cozy-stack-client "^45.2.0" + cozy-stack-client "^45.13.0" date-fns "2.29.3" json-stable-stringify "^1.0.1" lodash "^4.17.13" @@ -8017,10 +8017,10 @@ cozy-minilog@3.3.1: dependencies: microee "0.0.6" -cozy-stack-client@^45.2.0: - version "45.2.0" - resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-45.2.0.tgz#c0d20d6e11a57477752cd38f1ca8732c668f5d14" - integrity sha512-XpbD568C+gG2ZSkWcdlBQIfXb2kefpQpwjgMISZ0raHk6cA1c626pptfbadXzmNaj9IiDG/gTCztzbnxZ3xBUA== +cozy-stack-client@^45.13.0: + version "45.13.0" + resolved "https://registry.yarnpkg.com/cozy-stack-client/-/cozy-stack-client-45.13.0.tgz#394b21a153b752be99ac6c79d073159fd024a4f4" + integrity sha512-ruigDMH6XB1Pxa+e3doAMjXihgKSdNZpNPlEcpd7l8MfHazUaHVs/D8Kyop1n+VGM/4va2DU1c1nE7ainT7+jw== dependencies: detect-node "^2.0.4" mime "^2.4.0"