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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libs/API/parameters/SignInUserParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type SignInUserParams = {
preferredLocale: Locale | null;
validateCode?: string;
deviceInfo: string;
authToken?: string;
};

export default SignInUserParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/SignInUserWithLinkParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type SignInUserWithLinkParams = {
twoFactorAuthCode?: string;
preferredLocale: Locale | null;
deviceInfo: string;
authToken?: string;
};

export default SignInUserWithLinkParams;
21 changes: 19 additions & 2 deletions src/libs/actions/Session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,14 @@ function setIsAuthenticatingWithShortLivedToken(isAuthenticating: boolean) {
*
* @param validateCode - 6 digit code required for login
*/
function signIn(validateCode: string, preferredLocale: Locale | undefined, twoFactorAuthCode: string | undefined, login: string | undefined, storedValidateCode: string | undefined) {
function signIn(
validateCode: string,
preferredLocale: Locale | undefined,
twoFactorAuthCode: string | undefined,
login: string | undefined,
storedValidateCode: string | undefined,
storedAuthToken?: string,
) {
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.ACCOUNT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -954,11 +961,17 @@ function signIn(validateCode: string, preferredLocale: Locale | undefined, twoFa
params.validateCode = validateCode || storedValidateCode;
}

// If this is the 2FA step we have a short-lived 2FA authentication authToken from the earlier call, send it
// so the backend can complete the login
if (twoFactorAuthCode && storedAuthToken) {
params.authToken = storedAuthToken;
}

API.write(WRITE_COMMANDS.SIGN_IN_USER, params, {optimisticData, successData, failureData});
});
}

function signInWithValidateCode(accountID: number, code: string, preferredLocale: Locale | undefined, twoFactorAuthCode = '', storedValidateCode?: string) {
function signInWithValidateCode(accountID: number, code: string, preferredLocale: Locale | undefined, twoFactorAuthCode = '', storedValidateCode?: string, storedAuthToken?: string) {
// If this is called from the 2fa step, use the validateCode stored in Onyx (passed in as `storedValidateCode`)
// instead of the one passed from the component state because the state is changing when this method is called.
const validateCode = twoFactorAuthCode ? storedValidateCode : code;
Expand Down Expand Up @@ -1030,6 +1043,10 @@ function signInWithValidateCode(accountID: number, code: string, preferredLocale
deviceInfo,
};

if (twoFactorAuthCode && storedAuthToken) {
params.authToken = storedAuthToken;
}

API.write(WRITE_COMMANDS.SIGN_IN_USER_WITH_LINK, params, {optimisticData, successData, failureData});
});
}
Expand Down
5 changes: 3 additions & 2 deletions src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,9 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco

const accountID = credentials?.accountID;
if (accountID) {
signInWithValidateCode(accountID, validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.validateCode);
signInWithValidateCode(accountID, validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.validateCode, credentials?.authToken);
} else {
signIn(validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.login, credentials?.validateCode);
signIn(validateCode, preferredLocale, recoveryCodeOr2faCode, credentials?.login, credentials?.validateCode, credentials?.authToken);
}
}, [
account?.isLoading,
Expand All @@ -310,6 +310,7 @@ function BaseValidateCodeForm({autoComplete, isUsingRecoveryCode, setIsUsingReco
credentials?.validateCode,
credentials?.accountID,
credentials?.login,
credentials?.authToken,
isUsingRecoveryCode,
recoveryCode,
twoFactorAuthCode,
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ type Credentials = {
/** The validate code */
validateCode?: string;

/** The short-lived authToken issued when a 2FA code is required */
authToken?: string;

/** The auto-generated login. */
autoGeneratedLogin?: string;

Expand Down
45 changes: 45 additions & 0 deletions tests/actions/SessionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,29 @@ describe('Session', () => {

writeSpy.mockRestore();
});

test('sends the stored authToken during the 2FA step', async () => {
const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined);

SessionUtil.signIn('', undefined, '654321', 'user@expensify.com', 'stored-code', 'stored-auth-token');
await waitForBatchedUpdates();

expect(writeSpy.mock.calls.at(0)?.at(1)).toEqual(expect.objectContaining({authToken: 'stored-auth-token'}));

writeSpy.mockRestore();
});

test('does not send an authToken on the initial validate code submission', async () => {
const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined);

// No 2FA code yet, even though a stored authToken is passed in - shouldn't be sent.
SessionUtil.signIn('112233', undefined, undefined, 'user@expensify.com', undefined, 'stored-auth-token');
await waitForBatchedUpdates();

expect(writeSpy.mock.calls.at(0)?.at(1)).not.toHaveProperty('authToken');

writeSpy.mockRestore();
});
});

describe('requestUnlinkValidationLink', () => {
Expand Down Expand Up @@ -1214,5 +1237,27 @@ describe('Session', () => {

writeSpy.mockRestore();
});

test('sends the stored authToken during the 2FA step', async () => {
const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined);

SessionUtil.signInWithValidateCode(123, 'ignored-code', undefined, '654321', 'stored-code', 'stored-auth-token');
await waitForBatchedUpdates();

expect(writeSpy.mock.calls.at(0)?.at(1)).toEqual(expect.objectContaining({authToken: 'stored-auth-token'}));

writeSpy.mockRestore();
});

test('does not send an authToken on the initial validate code submission', async () => {
const writeSpy = jest.spyOn(API, 'write').mockResolvedValue(undefined);

SessionUtil.signInWithValidateCode(123, '112233', undefined, undefined, undefined, 'stored-auth-token');
await waitForBatchedUpdates();

expect(writeSpy.mock.calls.at(0)?.at(1)).not.toHaveProperty('authToken');

writeSpy.mockRestore();
});
});
});
Loading