App icon indicating copy to clipboard operation
App copied to clipboard

[$250] Session expired but user was not signed out

Open m-natarajan opened this issue 2 months ago โ€ข 26 comments

If you havenโ€™t already, check out our contributing guidelines for onboarding and email [email protected] to request to join our Slack channel!


Version Number: 9.2.43-0 Reproducible in staging?: Needs Reproduction (Unable to get session expired message) Reproducible in production?: Needs Reproduction If this was caught during regression testing, add the test name, ID and link from TestRail: Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by: @cead22 @coleaeason Slack conversation (hyperlinked to channel name): #Expensify Bugs

Action Performed:

  1. Log into staging web
  2. Wait until the session expires in the background
  3. Attempt any action (e.g., sending a message or opening a report)
  4. Observe that the request fails, but the user remains logged in

Expected Result:

Once the session expires, the app should automatically sign the user out or redirect them to the login screen with a clear message that the session has expired.

Actual Result:

The user remains on the app with an expired session and is not redirected to sign in again.

Workaround:

Unknown

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • [ ] Android: App
  • [ ] Android: mWeb Chrome
  • [ ] iOS: App
  • [ ] iOS: mWeb Safari
  • [ ] iOS: mWeb Chrome
  • [ ] Windows: Chrome
  • [x] MacOS: Chrome / Safari
  • [ ] MacOS: Desktop
Platforms Tested: On which of our officially supported platforms was this issue tested:
  • [ ] Android: App
  • [ ] Android: mWeb Chrome
  • [ ] iOS: App
  • [ ] iOS: mWeb Safari
  • [ ] iOS: mWeb Chrome
  • [x] Windows: Chrome
  • [ ] MacOS: Chrome / Safari
  • [ ] MacOS: Desktop

Screenshots/Videos

Add any screenshot/video evidence

The logs and screenshot for the issue is in OP

View all open jobs on GitHub

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~021986432883694552860
  • Upwork Job ID: 1986432883694552860
  • Last Price Increase: 2025-12-25
Issue OwnerCurrent Issue Owner: @abzokhattab

m-natarajan avatar Nov 05 '25 15:11 m-natarajan

Triggered auto assignment to @dylanexpensify (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.

melvin-bot[bot] avatar Nov 05 '25 15:11 melvin-bot[bot]

Job added to Upwork: https://www.upwork.com/jobs/~021986432883694552860

melvin-bot[bot] avatar Nov 06 '25 13:11 melvin-bot[bot]

Triggered auto assignment to Contributor-plus team member for initial proposal review - @abzokhattab (External)

melvin-bot[bot] avatar Nov 06 '25 13:11 melvin-bot[bot]

Proposal

Why this happens

When a userโ€™s authentication session/token expires, the UI still shows them as logged in, but all requests fail in the background. This results in a broken user state.

Root Cause

The frontend is not listening for:

  • HTTP 401 Unauthorized responses or
  • Token expiration time

If you want, I can provide solution code for your frontend and backend rules.

rodrigocost avatar Nov 06 '25 14:11 rodrigocost

๐Ÿ“ฃ @rodrigocost! ๐Ÿ“ฃ Hey, it seems we donโ€™t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork. Please follow these steps:

  1. Make sure you've read and understood the contributing guidelines.
  2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
  3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
  4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details. Screen Shot 2022-11-16 at 4 42 54 PM Format:
Contributor details
Your Expensify account email: <REPLACE EMAIL HERE>
Upwork Profile Link: <REPLACE LINK HERE>

melvin-bot[bot] avatar Nov 06 '25 14:11 melvin-bot[bot]

โš ๏ธ @rodrigocost Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

github-actions[bot] avatar Nov 06 '25 14:11 github-actions[bot]

Proposal

Please re-state the problem that we are trying to solve in this issue.

Session expired but user was not signed out

Expected Result: Once the session expires, the app should automatically sign the user out or redirect them to the login screen with a clear message that the session has expired.

Actual Result: The user remains on the app with an expired session and is not redirected to sign in again.

What is the root cause of that problem?

The root cause has two parts:

  1. Missing redirect when reauthentication fails silently: In Reauthentication.ts middleware, when reauthentication fails (wasSuccessful is false), the middleware returns early without ensuring the request is properly resolved. While redirectToSignIn() should have been called in Authentication.ts when reauthentication fails, there's no guarantee it was called in all failure scenarios.

  2. Missing redirect when reauthentication returns no response: In Authentication.ts, when the reauthentication request returns null or undefined (which can happen when the session has expired and credentials are invalid), the function returns false without calling redirectToSignIn(). This leaves the user in a logged-in state even though their session has expired.

What changes do you think we should make in order to solve the problem?

  1. Fix Reauthentication.ts middleware to properly handle reauthentication failures:

    • When wasSuccessful is false, ensure the request is properly resolved
    • Add comments explaining that redirectToSignIn() should have been called in Authentication.ts, but we need to ensure the request is handled correctly
    • This ensures that even if reauthentication fails, the request doesn't hang and the user experience is consistent
  2. Fix Authentication.ts to redirect when reauthentication returns no response:

    • When the reauthentication response is null or undefined, call redirectToSignIn('session.expired') before returning false
    • This ensures that when the session has expired and credentials are invalid, the user is properly signed out and redirected to the login screen
    • Set isAuthenticating to false to allow the app to process the redirect

These changes ensure that:

  • When a session expires and reauthentication fails, the user is always signed out
  • The user is redirected to the login screen with an appropriate error message
  • The error message "Your session has expired" is displayed on the sign-in page

Code Changes

1. src/libs/Middleware/Reauthentication.ts

Before:

return reauthenticate(request?.commandName)
    .then((wasSuccessful) => {
        if (!wasSuccessful) {
            return;
        }

        if (isFromSequentialQueue || apiRequestType === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) {
            return processWithMiddleware(request, isFromSequentialQueue);
        }

        if (apiRequestType === CONST.API_REQUEST_TYPE.READ) {
            NetworkConnection.triggerReconnectionCallbacks('read request made with expired authToken');
            return Promise.resolve();
        }

        replayMainQueue(request);
    })

After:

return reauthenticate(request?.commandName)
    .then((wasSuccessful) => {
        if (!wasSuccessful) {
            // Reauthentication failed - redirectToSignIn should have been called in Authentication.ts,
            // but we need to ensure the request is properly resolved and the user is signed out.
            // If we're here, it means the session has expired and we couldn't reauthenticate.
            if (isFromSequentialQueue) {
                return data;
            }

            if (request.resolve) {
                request.resolve(data);
            }
            return data;
        }

        if (isFromSequentialQueue || apiRequestType === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) {
            return processWithMiddleware(request, isFromSequentialQueue);
        }

        if (apiRequestType === CONST.API_REQUEST_TYPE.READ) {
            NetworkConnection.triggerReconnectionCallbacks('read request made with expired authToken');
            return Promise.resolve();
        }

        replayMainQueue(request);
    })

Changes:

  • Added proper handling when wasSuccessful is false
  • Ensures the request is properly resolved when reauthentication fails
  • Added comments explaining the behavior and why we need to handle this case
  • Returns the original data (which contains the NOT_AUTHENTICATED error) so the request is properly resolved

2. src/libs/Authentication.ts

Before:

}).then((response) => {
    if (!response) {
        return false;
    }

    Log.hmmm('Reauthenticate - Processing authentication result', {
        command,
    });

    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');
    }

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

After:

}).then((response) => {
    if (!response) {
        // If we get no response, the reauthentication request failed completely.
        // This could happen if the session has expired and credentials are invalid.
        setIsAuthenticating(false);
        Log.hmmm('Redirecting to Sign In because reauthentication returned no response', {
            command,
        });
        redirectToSignIn('session.expired');
        return false;
    }

    Log.hmmm('Reauthenticate - Processing authentication result', {
        command,
    });

    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');
    }

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

Changes:

  • Added handling for when response is null or undefined
  • Calls redirectToSignIn('session.expired') to sign out the user and redirect to login
  • Sets isAuthenticating to false to allow the app to process the redirect
  • Added comments explaining why this case needs to be handled
  • Uses the translation key 'session.expired' which displays "Your session has expired." on the sign-in page

What alternative solutions did you explore? (Optional)

  • Adding a session expiration check on every API request: We could check if the session is expired before making API requests, but this would add overhead and the server already handles this by returning NOT_AUTHENTICATED. The current approach of handling it in the middleware is more efficient.

IjaazA avatar Nov 07 '25 02:11 IjaazA

Hi @IjaazA I am struggling reproducing the issue... are u able to reproduce it?

abzokhattab avatar Nov 13 '25 14:11 abzokhattab

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Nov 13 '25 16:11 melvin-bot[bot]

@abzokhattab it is not reproducible now , I faced session expiry twice or thrice and took logs and with that i drafted rc and solution , but in recent main now I can't

IjaazA avatar Nov 13 '25 18:11 IjaazA

Can you please retest @cead22 @coleaeason

abzokhattab avatar Nov 17 '25 12:11 abzokhattab

I didn't experienced this today when coming back to new dot after a period of inactivity

cead22 avatar Nov 17 '25 21:11 cead22

@abzokhattab this just happened to me again. Let me know what info I can get to help troubleshoot. I shared some data in slack

cead22 avatar Nov 19 '25 16:11 cead22

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Nov 20 '25 16:11 melvin-bot[bot]

bumped on Slack for proposals :eyes: https://expensify.slack.com/archives/C01GTK53T8Q/p1763991419545099

abzokhattab avatar Nov 24 '25 13:11 abzokhattab

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Nov 27 '25 16:11 melvin-bot[bot]

@abzokhattab Whoops! This issue is 2 days overdue. Let's get this updated quick!

melvin-bot[bot] avatar Nov 28 '25 00:11 melvin-bot[bot]

@abzokhattab @dylanexpensify this issue is now 4 weeks old, please consider:

  • Finding a contributor to fix the bug
  • Closing the issue if BZ has been unable to add the issue to a VIP or Wave project
  • If you have any questions, don't hesitate to start a discussion in #expensify-open-source

Thanks!

melvin-bot[bot] avatar Dec 03 '25 22:12 melvin-bot[bot]

@abzokhattab Now this issue is 8 days overdue. Are you sure this should be a Daily? Feel free to change it!

melvin-bot[bot] avatar Dec 04 '25 00:12 melvin-bot[bot]

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Dec 04 '25 16:12 melvin-bot[bot]

@abzokhattab 10 days overdue. I'm getting more depressed than Marvin.

melvin-bot[bot] avatar Dec 05 '25 23:12 melvin-bot[bot]

Issue not reproducible during KI retests. (First week)

mvtglobally avatar Dec 06 '25 05:12 mvtglobally

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Dec 11 '25 16:12 melvin-bot[bot]

This issue has not been updated in over 14 days. @abzokhattab, @dylanexpensify eroding to Weekly issue.

melvin-bot[bot] avatar Dec 11 '25 23:12 melvin-bot[bot]

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Dec 18 '25 16:12 melvin-bot[bot]

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Dec 25 '25 16:12 melvin-bot[bot]

๐Ÿ“ฃ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? ๐Ÿ’ธ

melvin-bot[bot] avatar Jan 01 '26 16:01 melvin-bot[bot]

No decision yet ... there is still an active conversation on slack https://expensify.slack.com/archives/C01GTK53T8Q/p1763571739910709

cc @cead22 @VickyStash

abzokhattab avatar Jan 04 '26 16:01 abzokhattab

Not so long ago Jason also had similar issue (see this thread). I've tried to reproduce the problem on my end, but had no luck. @cead22 Does it still regularly happen to you?

VickyStash avatar Jan 07 '26 08:01 VickyStash