firebase / firebase-android-sdk

Firebase Android SDK
https://firebase.google.com
Apache License 2.0
2.28k stars 578 forks source link

Custom provider getToken() creates infinite loop when device time is tempered #3935

Open im12345dev opened 2 years ago

im12345dev commented 2 years ago

[READ] Step 1: Are you in the right place?

Issues filed here should be about bugs in the code in this repository. If you have a general question, need help debugging, or fall into some other category use one of these other channels:

[REQUIRED] Step 2: Describe your environment

[REQUIRED] Step 3: Describe the problem

I have implemented a custom provider using Firebase Functions and the firebase-admin sdk which return a token and expiration time using some custom logic, the implementation works perfectly when the time on the device is correct and not tempered with, but as soon as the time on the device is being tempered, for example a user set the clock to +2 hours using the device settings, Android App Check SDK gets into infinite loop as the token expiration time from the server is current time + 60 minutes but the device time is current time + 2 hours (due to settings change), which leads to the SDK to think the token is expired and start an infinite refresh loop

Steps to reproduce:

Configuring a token TTL to 60 minutes and changing the device clock to +1 hour should reproduce it

Relevant Code:

`public class YourCustomAppCheckProvider implements AppCheckProvider {

private final Context applicationContext;

public YourCustomAppCheckProvider(Context applicationContext) {
    this.applicationContext = applicationContext;
}

@Override
public Task<AppCheckToken> getToken() {

    TaskCompletionSource<AppCheckToken> taskCompletionSource = new TaskCompletionSource<>();

    try {
        String token = customLogicToGetToken()
        int expirationFromServer = expiration received from the server endpoint...

        long expMillis = expirationFromServer * 1000L - 60000;

        taskCompletionSource.setResult(new CustomAppCheckToken(token,expMillis));
    } catch (IOException | JSONException e) {
        taskCompletionSource.setException(e);
    }

    return taskCompletionSource.getTask();
}

} `

The Firebase function to generate the token + expiration time:

`

exports.fetchAppCheckToken = functions.https.onCall((authenticityData, context) => {

//Custom logic to decide if the request is valid or not, in case not return "error"...

    return admin.appCheck().createToken(APP ID)
        .then(function (appCheckToken) {
            const expiresAt = Math.floor(Date.now() / 1000) + 60 * 60;

            return {token: appCheckToken.token, expiresAt: expiresAt, ttlMillis: appCheckToken.ttlMillis}
        })
        .catch(function (err) {
            console.error("Unable to create App Check token.");
            console.error(err);

            return "error"
        });
});

Now of course I can just check if the "expiresAt" received from the function is smaller than System.currentTimeInMills() and then return exception in the getToken() method, but that means any user who changed his device time in settings wont be able to user my app. Is it a bug or just something I am doing wrong?

im12345dev commented 2 years ago

Hey @rizafran, did you manage to replicate it? it should be quite straight forward

argzdev commented 2 years ago

Hi @im12345dev, thanks for reporting. Sorry for the delayed response here. I tried testing this scenario using SafetyNet and it seems to work fine. I'm guessing this use case should also work when using a custom provider since the behavior for this scenario should be the same. Let me notify and consult our engineering team and see what we can do here. Thanks!

im12345dev commented 2 years ago

@argzdev I can replicate the issue on multiple devices using custom provider, the issue is coming from using System.currentTimeInMills() in the expire check, when you change the device time to +1 hour the System.currentTimeInMills() will change as well to +1 hour, causing the token to be expire immediately as the expire time is time in mills received from the server rather the device.

Also, isn't SafetyNet based on Play Services?

argzdev commented 2 years ago

Hi @im12345dev, yes, SafetyNet is based on Play Services. Sorry for the misunderstanding, what I'm trying to say is that if this type of use case works for SafetyNet, then I think we should also be supporting this in the custom provider setup as well.

I've raised this up to our engineering team, however while waiting for a response, could you provide more details on how you retrieve your getToken, specifically on the customLogicToGetToken() and expirationFromServer? I'm assuming this is when you call Firebase Functions. Can you share a bit more code regarding this? But of course, feel free to redact any PII or details you don't want to share. Other than that, this'll help us a lot.

Also if you could share a MCVE, that'll speed up our investigation even faster. Thanks!

Relevant code:

@Override
public Task<AppCheckToken> getToken() {

    TaskCompletionSource<AppCheckToken> taskCompletionSource = new TaskCompletionSource<>();

    try {
        String token = customLogicToGetToken()
        int expirationFromServer = expiration received from the server endpoint...

        long expMillis = expirationFromServer * 1000L - 60000;

        taskCompletionSource.setResult(new CustomAppCheckToken(token,expMillis));
    } catch (IOException | JSONException e) {
        taskCompletionSource.setException(e);
    }

    return taskCompletionSource.getTask();
}
im12345dev commented 2 years ago

Below is the getToken() code that I use to retrieve the token from the Firebase Function:

    @Override
    public Task<AppCheckToken> getToken() {

        Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://{project-id}.cloudfunctions.net")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
        MainActivity.APIService retroService = retrofit.create(MainActivity.APIService.class);

        Call<ResponseBody> call = retroService.appToken();
        TaskCompletionSource<AppCheckToken> taskCompletionSource = new TaskCompletionSource<>();

        try {
            JSONObject jsonObject = new JSONObject(call.execute().body().string());
            String token = jsonObject.getJSONObject("result").getString("token");
            int expirationFromServer = jsonObject.getJSONObject("result").getInt("expiresAt");
            long expMillis = expirationFromServer * 1000L - 60000;

            taskCompletionSource.setResult(new CustomAppCheckToken(token,expMillis));
        } catch (IOException | JSONException e) {
            taskCompletionSource.setException(e);
        }

        return taskCompletionSource.getTask();
    }

This is the Firebase Functions I use to generate the token and send the information back to the client:

exports.fetchAppCheckToken = functions.https.onCall((authenticityData, context) => {
    console.log('authenticityData: '+JSON.stringify(authenticityData))

    return admin.appCheck().createToken('{app-id}')
        .then(function (appCheckToken) {
            // Token expires in an hour.
            const expiresAt = Math.floor(Date.now() / 1000) + 60 * 60;
            console.log('Token: '+JSON.stringify(appCheckToken))
            console.log('expiresAt: '+expiresAt)

            return {token: appCheckToken.token, expiresAt: expiresAt, ttlMillis: appCheckToken.ttlMillis}
        })
        .catch(function (err) {
            console.error('Unable to create App Check token.');
            console.error(err);

            return "error"
        });
});

I am using RetroFit to call the https link of the Firebase Functions and retrieve the data:

  1. JSON object containing the token & expire time in milliseconds
  2. Error incase something went wrong

The expire time the Firebase Functions sends together with the token is: Math.floor(Date.now() / 1000) + 60 * 60 which is exactly the same as the docs

argzdev commented 2 years ago

Thanks for the extra details, @im12345dev. I'm running into NetworkOnMainThreadException on your code snippet, so I tweaked it a bit. However, I'm still unable to reproduce the same behavior even after adding 1 or 2 hrs to the physical/emulator device time.

Relevant code:

public Task<AppCheckToken> getToken() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("https://{project-id}.cloudfunctions.net")
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        APIService retroService = retrofit.create(APIService.class);

        Call<ResponseBody> call = null;
        try {
            call = retroService.appToken(new DataRequest(new JSONObject().put("data", null)));
        } catch (JSONException e) {
            e.printStackTrace();
        }

        TaskCompletionSource<AppCheckToken> taskCompletionSource = new TaskCompletionSource<>();

        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<okhttp3.ResponseBody> call, Response<ResponseBody> response) {
                try {
                    JSONObject jsonObject = new JSONObject(response.body().string());
                    String token = jsonObject.getJSONObject("result").getString("token");
                    int expirationFromServer = jsonObject.getJSONObject("result").getInt("expiresAt");
                    long expMillis = expirationFromServer * 1000L - 60000;

                    Log.d(TAG, "onResponse: " + response.body().string());

                    taskCompletionSource.setResult(new YourCustomAppCheckToken(token,expMillis));
                } catch (IOException | JSONException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Log.d(TAG, "onFailure: " + t.getMessage());
            }
        });

        return taskCompletionSource.getTask();
    }

To test this, I called this via a button click:

private void retrieveTokenAuto(){
        firebaseAppCheck.getToken(true).addOnCompleteListener(task -> {
            if(task.isSuccessful()){
                String token = task.getResult().getToken();
                Log.d(TAG, "token received: " + token);
            } else {
                Log.d(TAG, "token retrieval failed: ");
            }
        });
    }

Am I missing anything?

im12345dev commented 2 years ago

@argzdev Using the exact same code you posted I was able to replicate it, it goes into infinite loop of renewing the token. Can you post the Firebase Function you used in order to generate the token?

In this video you can see the infinite loop been created, using the same code as yours, I just added 2 hours to the emulator time: https://user-images.githubusercontent.com/91768057/184416004-f22f86ca-3604-4987-abc0-50256f8723f6.mov

Clicking on the Fab button in the video is starting the retriveTokenAuto() you posted

im12345dev commented 2 years ago

I just noticed that the App Check Provider is checking if the token is expired by the expiration time in mills inside the token itself, so what is the purpose of adding expiration time to your Custom Token object?

argzdev commented 2 years ago

I've used the following for my Firebase functions:

exports.fetchAppCheckToken = firebasefunctions.https.onCall((data, context) => {
  const appId = "your_project_app_id";

  return admin.appCheck().createToken(appId)
      .then(function(appCheckToken) {
        const expiresAt = Math.floor(Date.now() / 1000) + 60 * 60;

        const token = {
          token: appCheckToken.token,
          expiresAt: expiresAt,
          ttlMillis: appCheckToken.ttlMillis,
        };

        firebasefunctions.logger.info(token, {structuredData: true});

        return token;
      })
      .catch(function(err) {
        console.error("Unable to create App Check token.");
        console.error(err);
        return "error";
      });
});

I'm not sure why I'm unable to experience the same behavior as yours, maybe something is missing on my setup. Could you provide a minimal reproducible example instead, so I can investigate this further?

Also can you point me to the code snippet in the SDK, where you think this is being done? So I can consult with an engineer regarding your question.

App Check Provider is checking if the token is expired by the expiration time in mills inside the token itself

Thanks in advance!

google-oss-bot commented 2 years ago

Hey @im12345dev. We need more information to resolve this issue but there hasn't been an update in 5 weekdays. I'm marking the issue as stale and if there are no new updates in the next 5 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

im12345dev commented 2 years ago

Sorry for the late response @argzdev , here is a minimal reproducible example: MainActivity onCreate where fab2 is the button that retrieve the token

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        FirebaseApp firebaseApp = FirebaseApp.initializeApp(/*context=*/ this);
        FirebaseFirestore db = FirebaseFirestore.getInstance();
        firebaseAppCheck = FirebaseAppCheck.getInstance();
        firebaseAppCheck.installAppCheckProviderFactory(YourCustomAppCheckProviderFactory.getInstance(this));

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        binding.fab2.setOnClickListener(view -> {
            firebaseAppCheck.getToken(true).addOnCompleteListener(task -> {
                if(task.isSuccessful()){
                    String token = task.getResult().getToken();
                    Log.d(TAG, "token received: " + token);
                } else {
                    Log.d(TAG, "token retrieval failed: ");
                }
            });
        });
    }

My custom provider factory:

public class YourCustomAppCheckProviderFactory implements AppCheckProviderFactory {

    private static YourCustomAppCheckProviderFactory yourCustomAppCheckProviderFactory;
    private final Context applicationContext;

    public YourCustomAppCheckProviderFactory(Context applicationContext) {
        this.applicationContext = applicationContext;
    }

    public static YourCustomAppCheckProviderFactory getInstance(Context applicationContext){
        if (yourCustomAppCheckProviderFactory == null)
            yourCustomAppCheckProviderFactory = new YourCustomAppCheckProviderFactory(applicationContext);
        return yourCustomAppCheckProviderFactory;
    }

    @NonNull
    @Override
    public AppCheckProvider create(@NonNull FirebaseApp firebaseApp) {
        // Create and return an AppCheckProvider object.
        return new MyAppCheckProvider(firebaseApp);
    }
}

The app check provider:

public class YourCustomAppCheckProvider implements AppCheckProvider {

    private final Context applicationContext;

    public YourCustomAppCheckProvider(Context applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public Task<AppCheckToken> getToken() {

        Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("{BASE_URL}")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
        MainActivity.APIService retroService = retrofit.create(MainActivity.APIService.class);

        Call<ResponseBody> call = retroService.appToken();
        TaskCompletionSource<AppCheckToken> taskCompletionSource = new TaskCompletionSource<>();

        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<okhttp3.ResponseBody> call, Response<ResponseBody> response) {
                try {
                    JSONObject jsonObject = new JSONObject(response.body().string());
                    String token = jsonObject.getJSONObject("result").getString("token");
                    int expirationFromServer = jsonObject.getJSONObject("result").getInt("expiresAt");
                    long expMillis = expirationFromServer * 1000L - 60000;

                    taskCompletionSource.setResult(new CustomAppCheckToken(token,expMillis));
                } catch (IOException | JSONException e) {
                    taskCompletionSource.setException(e);
                }
            }
            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Log.d(MainActivity.TAG, "onFailure: " + t.getMessage());
            }
        });

        return taskCompletionSource.getTask();
    }
}

The app check token object:

public class CustomAppCheckToken extends AppCheckToken {
    private final String token;
    private final long expiration;

    CustomAppCheckToken(String token, long expiration) {
        this.token = token;
        this.expiration = expiration;
    }

    @NonNull
    @Override
    public String getToken() {
        return token;
    }

    @Override
    public long getExpireTimeMillis() {
        return expiration;
    }

}

For the firebase functions I used the same code as you used:

exports.fetchAppCheckToken = firebasefunctions.https.onCall((data, context) => {
  const appId = "your_project_app_id";

  return admin.appCheck().createToken(appId)
      .then(function(appCheckToken) {
        const expiresAt = Math.floor(Date.now() / 1000) + 60 * 60;

        const token = {
          token: appCheckToken.token,
          expiresAt: expiresAt,
          ttlMillis: appCheckToken.ttlMillis,
        };

        firebasefunctions.logger.info(token, {structuredData: true});

        return token;
      })
      .catch(function(err) {
        console.error("Unable to create App Check token.");
        console.error(err);
        return "error";
      });
});
google-oss-bot commented 2 years ago

Since there haven't been any recent updates here, I am going to close this issue.

@im12345dev if you're still experiencing this problem and want to continue the discussion just leave a comment here and we are happy to re-open this.

argzdev commented 2 years ago

Thanks for the extra details, @im12345dev. I noticed that you're using MyAppCheckProvider instead of the YourCustomAppCheckProvider? Can you share your code from that file?

Since the getToken() goes to an infinite loop, does your Cloud function spams the logs with multiple requests from fetching the token? e.g. fetchAppCheckToken

Upon further investigation, when the time is tempered, the AppCheckListener will keep refreshing itself to retrieve a new token.

firebaseAppCheck.addAppCheckListener(token -> {
            Log.d(TAG, "onAppCheckTokenChanged: " + token.getToken());
            Log.d(TAG, "onAppCheckTokenChanged: " + token.getExpireTimeMillis());
        });

I think this is similar to what you're experiencing. I'll inform our engineers with these extra findings.

im12345dev commented 2 years ago

Returning MyAppCheckProvider instead of YourCustomAppCheckProvider was indeed an overlook by my part, unfortunately changing it didn't solve the issue.

As for your questions, yes the Cloud function spam the logs with each token created/refreshed.

I am glad you managed to reproduce it, let me know if you need more information, hopefully it can be patched quickly in the next release as this is the only thing preventing us from migrating to App Check

im12345dev commented 2 years ago

Hey @argzdev, any news regarding this issue?

argzdev commented 2 years ago

HI @im12345dev, sorry for the lack of response here. Our engineers are still in the process of brainstorming on how to deal with this type of use case. There hasn't been any significant progress yet. We'll respond back here once we get an update.

This is internally tracked at: b/241889048

Thank you for your patience.

im12345dev commented 1 year ago

Hey @argzdev, any news regarding the issue?

im12345dev commented 1 year ago

@argzdev haven't heard back in a while, any news?

im12345dev commented 1 year ago

@rizafran @argzdev @sheepmaster It's been 6 months now and I haven't heard back at all, this type of bug should prevent anyone from using the custom providers as any misconfigured device can trigger millions of unnecessary cloud functions calls, is there any news?

argzdev commented 1 year ago

Hi @im12345dev, thanks for following up on us. We had some investigations and discussions regarding this, and it seems like were taking a step back, note that App Check does not support tampering with device time. Doing so signals that the app has been tampered with.

That said, you've mentioned that misconfigured device can trigger millions of unnecessary cloud functions calls. I wasn't able to reproduce this behavior that you've mentioned.

Upon further investigation, when the time is tempered, the following behavior is experienced.

I also noticed that it's been sometime since this issue has been filed, and com.google.firebase:firebase-bom version 30.2.0 was being used. Could you try upgrading to the latest version 31.4.0, and see if you'll experience a different behavior, specifically that the getToken() should only be called once.

If you're still experiencing the same behavior after the update, it might mean we're missing something. Please upload your code into your repository and share it with us so we can investigate this further.

im12345dev commented 1 year ago

@argzdev Do you disable your auto token refresh? (firebaseAppCheck.setTokenAutoRefreshEnabled(false);)

argzdev commented 1 year ago

No, I didn't disable auto token refresh. Does using the latest version 31.4.0 have any effect?

im12345dev commented 1 year ago

31.4.0 does not have any affect, disabling the auto refresh solves the issue (kinda), but I would just want to make sure, if the auto refresh is disabled do I need to manually refresh the token? or the getToken() will be called every time there will be an Firestore operation with invalid token? (resulting in correct refresh)

argzdev commented 1 year ago

the auto refresh is disabled do I need to manually refresh the token?

I'm not sure if that is the correct strategy for this. If App Check does will not support tampering devices, then I think we should atleast have a logic strategy handling these types of issues. I'll discuss with our engineers to see what we can do.

Also after further investigating, I was able to reproduce the infinite calling of getToken(). Given with this much evidence, I can probably justify an engineer’s time to dig into this further.

im12345dev commented 1 year ago

That's great news, thank you for informing me. In the meanwhile we are performing tests with firebaseAppCheck.setTokenAutoRefreshEnabled(false); with a large set of beta users, although the solution isn't perfect, the App Check library seems to refresh the token when there is a Firestore read/write and the token is invalid, the refresh happens correctly regardless of the time of the device (and happens only once).

We believe that combining the above mentioned with long TTL (24 hours) for the tokens might reduce/eliminate any invalid tokens.

The devices in question are Amazon's devices, as we have a huge set of users using Amazon tablets and as you know the official Play Integrity provider isn't supported on them.

im12345dev commented 1 year ago

Hey @argzdev, I can see that version 17 of app check was released, was this issue addressed in that version?

argzdev commented 1 year ago

Hi @im12345dev, apologies for the lack of updates here. Upon checking the internal bug indicates that the investigation is still on going. So I believe this issue was not addressed in the latest version. I'll bump up the internal bug again to get some feed back.

Detached-BebHEAD commented 1 year ago

@argzdev Any updates on this? I'm running into the same issue; Even with firebaseAppCheck.setTokenAutoRefreshEnabled(false), and using a super long TTL from my custom provider, getToken() is getting called non-stop for me

im12345dev commented 1 year ago

@Detached-BebHEAD with firebaseAppCheck.setTokenAutoRefreshEnabled(false) the SDK should ask for a new token only once every time there is a use of the Firestore SDK (read or write), resulting in X calls per X actions (read/write), in our case it's tolerate able although we can already see cases of execsive spam to our Firebase Function handling generating the token, so far we can live with it but obviously this isn't ideal (or the right way of doing it) and I am shocked this wasn't considered in the first place as it's one of the firsts test we came up with while implementing App Check in our projects.

belcrod5 commented 1 year ago

@im12345dev, @argzdev, @rizafran

Title: Solving Issues with expireTimeMillis in Firebase's App Check Token

If you are encountering issues when setting expireTimeMillis in Firebase's App Check Token, the problem could be due to a misunderstanding of what the expireTimeMillis value should represent.

expireTimeMillis should be the exact Unix timestamp (in milliseconds) when the token will expire. It's not the "time to live" (TTL) or duration until the token expires, but rather the exact point in future time when the token becomes invalid.

Thus, if you're getting the token's TTL (time to live) from your server and attempting to set expireTimeMillis directly as *ttlMillis 1000**, you might run into problems because you're not accounting for the current time.

To fix this, you should calculate expireTimeMillis by adding the current Unix timestamp to the TTL value from your server (converting to milliseconds if needed). Here's a sample calculation:

const now = Date.now();  // Current time in milliseconds
const ttlMillis = ttlFromServer * 1000;  // Convert TTL from server to milliseconds
const expireTimeMillis = now + ttlMillis;  // Future Unix timestamp when the token will expire

By setting expireTimeMillis this way, you define an exact point in future time when the token will become invalid, and it should resolve the issues you're facing.

I hope this helps! Please let me know if you have any other questions.

im12345dev commented 1 year ago

Thank you for your input @belcrod5, but it's not quite correct. The expireTimeMillis you set in your custom provider is quite redundant as the app-check library is using the expireTimeMillis encoded in the token received from the server to check if the token is expired, when it see that it expires it will go into infinite loop if the time of the device is not correct (for example it's 1/2 days ahead of current time).

The above mentioned is causing the issue, you can modify the expire time in the custom provider according to the device time & date but unfortunately it will have no affect whatsoever.

im12345dev commented 1 year ago

Any update on the issue @argzdev

argzdev commented 1 year ago

Sorry @im12345dev, upon checking the internal bug link, there hasn't been any updates yet.

nohe427 commented 1 year ago

Thank you for your input @belcrod5, but it's not quite correct. The expireTimeMillis you set in your custom provider is quite redundant as the app-check library is using the expireTimeMillis encoded in the token received from the server to check if the token is expired, when it see that it expires it will go into infinite loop if the time of the device is not correct (for example it's 1/2 days ahead of current time).

The above mentioned is causing the issue, you can modify the expire time in the custom provider according to the device time & date but unfortunately it will have no affect whatsoever.

@im12345dev - Can you share the exact expireTimeMillis that you are using in your token when it goes into the infinite loop?

Esnoan commented 1 month ago

@im12345dev, @argzdev, @rizafran

Title: Solving Issues with expireTimeMillis in Firebase's App Check Token

If you are encountering issues when setting expireTimeMillis in Firebase's App Check Token, the problem could be due to a misunderstanding of what the expireTimeMillis value should represent.

expireTimeMillis should be the exact Unix timestamp (in milliseconds) when the token will expire. It's not the "time to live" (TTL) or duration until the token expires, but rather the exact point in future time when the token becomes invalid.

Thus, if you're getting the token's TTL (time to live) from your server and attempting to set expireTimeMillis directly as *ttlMillis 1000**, you might run into problems because you're not accounting for the current time.

To fix this, you should calculate expireTimeMillis by adding the current Unix timestamp to the TTL value from your server (converting to milliseconds if needed). Here's a sample calculation:

const now = Date.now();  // Current time in milliseconds
const ttlMillis = ttlFromServer * 1000;  // Convert TTL from server to milliseconds
const expireTimeMillis = now + ttlMillis;  // Future Unix timestamp when the token will expire

By setting expireTimeMillis this way, you define an exact point in future time when the token will become invalid, and it should resolve the issues you're facing.

I hope this helps! Please let me know if you have any other questions.

Thx, works for me