Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NoQA] Fix reauthentication #52727

Merged
merged 21 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const ONYXKEYS = {
PERSISTED_REQUESTS: 'networkRequestQueue',
PERSISTED_ONGOING_REQUESTS: 'networkOngoingRequestQueue',

/** The re-authentication request to be retried as needed */
REAUTHENTICATION_REQUEST: 'reauthenticationRequest',

/** Stores current date */
CURRENT_DATE: 'currentDate',

Expand Down Expand Up @@ -888,6 +891,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.IS_SIDEBAR_LOADED]: boolean;
[ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[];
[ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: OnyxTypes.Request;
[ONYXKEYS.REAUTHENTICATION_REQUEST]: OnyxTypes.Request;
[ONYXKEYS.CURRENT_DATE]: string;
[ONYXKEYS.CREDENTIALS]: OnyxTypes.Credentials;
[ONYXKEYS.STASHED_CREDENTIALS]: OnyxTypes.Credentials;
Expand Down
77 changes: 33 additions & 44 deletions src/libs/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,55 +62,44 @@ function reauthenticate(command = ''): Promise<void> {
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: credentials?.autoGeneratedLogin,
partnerUserSecret: credentials?.autoGeneratedPassword,
})
.then((response) => {
if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) {
// If authentication fails, then the network can be unpaused
NetworkStore.setIsAuthenticating(false);
}).then((response) => {
if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) {
// When a fetch() fails due to a network issue and an error is thrown we won't log the user out. Most likely they
// have a spotty connection and will need to retry reauthenticate when they come back online. Error so it can be handled by the retry mechanism.
throw new Error('Unable to retry Authenticate request');
}

// When a fetch() fails due to a network issue and an error is thrown we won't log the user out. Most likely they
// have a spotty connection and will need to try to reauthenticate when they come back online. We will error so it
// can be handled by callers of reauthenticate().
throw new Error('Unable to retry Authenticate request');
}

// If authentication fails and we are online then log the user out
if (response.jsonCode !== 200) {
const errorMessage = ErrorUtils.getAuthenticateErrorMessage(response);
NetworkStore.setIsAuthenticating(false);
Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', {
command,
error: errorMessage,
});
redirectToSignIn(errorMessage);
return;
}
// If authentication fails and we are online then log the user out
if (response.jsonCode !== 200) {
const errorMessage = ErrorUtils.getAuthenticateErrorMessage(response);
NetworkStore.setIsAuthenticating(false);
Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', {
command,
error: errorMessage,
});
redirectToSignIn(errorMessage);
return;
}

// If we reauthenticated due to an expired delegate token, restore the delegate's original account.
// This is because the credentials used to reauthenticate were for the delegate's original account, and not for the account they were connected as.
if (Delegate.isConnectedAsDelegate()) {
Log.info('Reauthenticated while connected as a delegate. Restoring original account.');
Delegate.restoreDelegateSession(response);
return;
}
// If we reauthenticated due to an expired delegate token, restore the delegate's original account.
// This is because the credentials used to reauthenticate were for the delegate's original account, and not for the account they were connected as.
if (Delegate.isConnectedAsDelegate()) {
Log.info('Reauthenticated while connected as a delegate. Restoring original account.');
Delegate.restoreDelegateSession(response);
return;
}

// Update authToken in Onyx and in our local variables so that API requests will use the new authToken
updateSessionAuthTokens(response.authToken, response.encryptedAuthToken);
// Update authToken in Onyx and in our local variables so that API requests will use the new authToken
updateSessionAuthTokens(response.authToken, response.encryptedAuthToken);

// Note: It is important to manually set the authToken that is in the store here since any requests that are hooked into
// reauthenticate .then() will immediate post and use the local authToken. Onyx updates subscribers lately so it is not
// enough to do the updateSessionAuthTokens() call above.
NetworkStore.setAuthToken(response.authToken ?? null);
// Note: It is important to manually set the authToken that is in the store here since any requests that are hooked into
// reauthenticate .then() will immediate post and use the local authToken. Onyx updates subscribers lately so it is not
// enough to do the updateSessionAuthTokens() call above.
NetworkStore.setAuthToken(response.authToken ?? null);

// The authentication process is finished so the network can be unpaused to continue processing requests
NetworkStore.setIsAuthenticating(false);
})
.catch((error) => {
// In case the authenticate call throws error, we need to sign user out as most likely they are missing credentials
NetworkStore.setIsAuthenticating(false);
Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', {error});
redirectToSignIn('passwordForm.error.fallback');
});
// The authentication process is finished so the network can be unpaused to continue processing requests
NetworkStore.setIsAuthenticating(false);
});
}

export {reauthenticate, Authenticate};
37 changes: 34 additions & 3 deletions src/libs/Middleware/Reauthentication.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import Onyx from 'react-native-onyx';
import redirectToSignIn from '@libs/actions/SignInRedirect';
import * as Authentication from '@libs/Authentication';
import Log from '@libs/Log';
import * as MainQueue from '@libs/Network/MainQueue';
import * as NetworkStore from '@libs/Network/NetworkStore';
import type {RequestError} from '@libs/Network/SequentialQueue';
import NetworkConnection from '@libs/NetworkConnection';
import * as Request from '@libs/Request';
import CONST from '@src/CONST';
import * as RequestThrottle from '@src/libs/RequestThrottle';
import ONYXKEYS from '@src/ONYXKEYS';
import type Middleware from './types';

// We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time.
Expand All @@ -15,19 +20,44 @@ function reauthenticate(commandName?: string): Promise<void> {
return isAuthenticating;
}

isAuthenticating = Authentication.reauthenticate(commandName)
const reauthRequest = {
commandName,
};
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, reauthRequest);
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved

isAuthenticating = retryReauthenticate(commandName)
.then((response) => {
isAuthenticating = null;
return response;
})
.catch((error) => {
isAuthenticating = null;
throw error;
})
.finally(() => {
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.set(ONYXKEYS.REAUTHENTICATION_REQUEST, null);
isAuthenticating = null;
});

return isAuthenticating;
}

function retryReauthenticate(commandName?: string): Promise<void> {
return Authentication.reauthenticate(commandName).catch((error: RequestError) => {
return RequestThrottle.sleep(error, 'Authenticate')
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
.then(() => retryReauthenticate(commandName))
.catch(() => {
NetworkStore.setIsAuthenticating(false);
Log.hmmm('Redirecting to Sign In because we failed to reauthenticate after multiple attempts', {error});
redirectToSignIn('passwordForm.error.fallback');
});
});
}

function resetReauthentication(): void {
isAuthenticating = null;
}

const Reauthentication: Middleware = (response, request, isFromSequentialQueue) =>
response
.then((data) => {
Expand Down Expand Up @@ -118,3 +148,4 @@ const Reauthentication: Middleware = (response, request, isFromSequentialQueue)
});

export default Reauthentication;
export {reauthenticate, resetReauthentication};
14 changes: 12 additions & 2 deletions src/libs/Network/SequentialQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Onyx from 'react-native-onyx';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import Log from '@libs/Log';
import * as Request from '@libs/Request';
import * as RequestThrottle from '@libs/RequestThrottle';
import * as PersistedRequests from '@userActions/PersistedRequests';
import * as QueuedOnyxUpdates from '@userActions/QueuedOnyxUpdates';
import CONST from '@src/CONST';
import * as RequestThrottle from '@src/libs/RequestThrottle';
import ONYXKEYS from '@src/ONYXKEYS';
import type OnyxRequest from '@src/types/onyx/Request';
import type {ConflictData} from '@src/types/onyx/Request';
Expand Down Expand Up @@ -271,5 +271,15 @@ function waitForIdle(): Promise<unknown> {
return isReadyPromise;
}

export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process};
function resetQueue(): void {
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
isSequentialQueueRunning = false;
currentRequestPromise = null;
isQueuePaused = false;
isReadyPromise = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
});
resolveIsReadyPromise?.();
}

export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause, process, resetQueue};
export type {RequestError};
17 changes: 12 additions & 5 deletions src/libs/Network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@ import pkg from '../../../package.json';
import * as MainQueue from './MainQueue';
import * as SequentialQueue from './SequentialQueue';

let processQueueInterval: NodeJS.Timer;
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved

// We must wait until the ActiveClientManager is ready so that we ensure only the "leader" tab processes any persisted requests
ActiveClientManager.isReady().then(() => {
SequentialQueue.flush();

// Start main queue and process once every n ms delay
setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS);
processQueueInterval = setInterval(MainQueue.process, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS);
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
});

// Clear interval
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
function clearProcessQueueInterval() {
if (!processQueueInterval) {
return;
}
clearInterval(processQueueInterval as unknown as number);
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Perform a queued post request
*/
Expand Down Expand Up @@ -55,7 +65,4 @@ function post(command: string, data: Record<string, unknown> = {}, type = CONST.
});
}

export {
// eslint-disable-next-line import/prefer-default-export
post,
};
export {post, clearProcessQueueInterval};
1 change: 1 addition & 0 deletions tests/unit/APITest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ beforeEach(() => {
MainQueue.clear();
HttpUtils.cancelPendingRequests();
PersistedRequests.clear();
RequestThrottle.clear();
NetworkStore.checkRequiredData();

// Wait for any Log command to finish and Onyx to fully clear
Expand Down
Loading
Loading