logto-io / js

🤓 Logto JS SDKs.
https://docs.logto.io/quick-starts/
MIT License
61 stars 40 forks source link

bug: (Nuxt) endless loading after the ID token expires #732

Closed ysemennikov closed 2 months ago

ysemennikov commented 3 months ago

Describe the bug

After I sign in and the given ID token expires, it seems like it's not being refreshed. This results in endless loading of the page. However, when I go to the sign-out route (domain.com/sign-out), I successfully log out and the page then loads successfully again.

Expected behavior

The page loading is not blocked. The token has to be refreshed.

How to reproduce?

  1. Sign in to the account using Logto
  2. Wait until the token expires
  3. Try to load the page again

app.vue:

<script setup lang="ts">
const client = useLogtoClient();
const user = useLogtoUser();

const idToken = useState<string | null>('id_token');

const currentOrgID = useCookie<string | undefined>('organization_id');
const currentOrgToken = useState<string | undefined>('organization_token');

if (user) {
  const organizationData: { id: string; name: string; description: string | null }[] = user.organization_data;
  const organizationIDs = organizationData.map(org => org.id);

  if (!organizationIDs.includes(currentOrgID.value!)) {
    currentOrgID.value = undefined;
  }

  if (!currentOrgID.value && organizationData.length) {
    currentOrgID.value = organizationData[0].id;
  }
}

await callOnce(async () => {
  if (!client) {
    throw new Error('Logto client is not available');
  }

  const [idTokenRes, orgTokenRes] = await Promise.allSettled([
    client.getIdToken(),
    currentOrgID.value ? client.getOrganizationToken(currentOrgID.value) : Promise.resolve(undefined),
  ]);

  if (idTokenRes.status === 'fulfilled') {
    idToken.value = idTokenRes.value;
  }
  if (orgTokenRes.status === 'fulfilled') {
    currentOrgToken.value = orgTokenRes.value;
  }
});
</script>

<template>
  <NuxtLoadingIndicator color="#409EFF" />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Context

Nuxt 3 app with Logto module installed, deployed on Vercel. We also use Organizations feature of Logto.

Screenshots

image image
ysemennikov commented 3 months ago

Hi @wangsijie, thanks for assigning yourself :) If you need, I can create an account for you, so you can test it yourself on my website

ysemennikov commented 3 months ago

Small update: I tried to debug this and localize the issue.

With this app.vue the website loads properly (so I removed the logic of receiving tokens, but, of course, this means that auth is disabled):

<template>
  <NuxtLoadingIndicator color="#409EFF" />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

And this app.vue causes the issue:

<script setup lang="ts">
const client = useLogtoClient();

const idToken = useState<string | null>('id_token', () => null);
const currentOrgID = useCookie<string | undefined>('organization_id');

await callOnce(async () => {
  if (!client) {
    throw new Error('Logto client is not available');
  }

  const [idTokenRes, orgTokenRes] = await Promise.allSettled([
    client.getIdToken(),
    currentOrgID.value ? client.getOrganizationToken(currentOrgID.value) : Promise.resolve(null),
  ]);

  if (idTokenRes.status === 'fulfilled') {
    idToken.value = idTokenRes.value;
  } else {
    idToken.value = null;
  }
});
</script>

<template>
  <NuxtLoadingIndicator color="#409EFF" />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
ysemennikov commented 3 months ago

My hypothesis is that maybe the client.getOrganizationToken method requires client.getIdToken to be called before?

So I'm using the Promise.allSettled to avoid sequential tokens fetching (I wanted them to be fetched in parallel). Maybe I should do something like this instead of Promise.allSettled:

idToken.value = await client.getIdToken();
currentOrgToken.value = await client.getOrganizationToken(currentOrgID.value)

Will test this soon.

wangsijie commented 3 months ago

I'll take a look.

ysemennikov commented 3 months ago

Hey! Some new updates are here. Try to tested my hypothesis

My hypothesis is that maybe the client.getOrganizationToken method requires client.getIdToken to be called before?

With this app.vue, and the issue has appeared again (infinite loading of the page). So it seems like the client.getOrganizationToken causes the error.

<script setup lang="ts">
const client = useLogtoClient();
const user = useLogtoUser();

const idToken = useState<string | null>('id_token', () => null);
const currentOrgID = useCookie<string | undefined>('organization_id');
const currentOrgToken = useState<string | null>('organization_token', () => null);

if (user) {
  await callOnce(async () => {
    if (!client) {
      throw new Error('Logto client is not available. Failed to fetch id_token and access_token.');
    }

    // Get organizations list of current user
    const organizationData: { id: string; name: string; description: string | null }[] = user.organization_data;
    const organizationIDs = organizationData.map(org => org.id);

    // if currentOrgID is not in the list of organizations, reset it
    if (!organizationIDs.includes(currentOrgID.value!)) {
      currentOrgID.value = undefined;
    }

    // if currentOrgID is not set, set it to the first organization
    if (!currentOrgID.value && organizationData.length) {
      currentOrgID.value = organizationData[0].id;
    }

    // fetch ID token
    idToken.value = await client.getIdToken();

    // fetch organization access token
    if (currentOrgID.value) {
      currentOrgToken.value = await client.getOrganizationToken(currentOrgID.value);
    }
  });
}
</script>

<template>
  <NuxtLoadingIndicator color="#409EFF" />
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

I will try to remove the currentOrgToken.value = await client.getOrganizationToken(currentOrgID.value); string and test again.

ysemennikov commented 3 months ago

I will try to remove the currentOrgToken.value = await client.getOrganizationToken(currentOrgID.value); string and test again.

Hey @wangsijie, I have tried this and it works. So the client.getOrganizationToken causes the endless loading

ysemennikov commented 3 months ago

Hi @wangsijie, do you have any updates regarding this? I tried to take a look myself, but haven't figured out what the problem might be yet. As I understand, something is wrong in @logto/client, maybe you could point me so I could find a solution? Unfortunately it's kind of a blocker for us now, because it's impossible to release our app if the page simply doesn't load :(

wangsijie commented 3 months ago

Thanks for your patient, I am looking into this today.

wangsijie commented 3 months ago

hi @ysemennikov, I tried but I am unable to reproduct the "endless loading", after the local tokens expired, trying to "getOrganizationToken" will throw a 500 error in nuxt app, and it won't redirect to sign in page automaticlly. I guess you have some error handlers that will catch the error and do the redirection?

wangsijie commented 3 months ago

btw, the SDK itself is unable to detect the "expiration", even if the idToken is invalid, isAuthticated() still returns true, it only check if idToken exists.

https://github.com/logto-io/js/blob/master/packages/client/src/client.ts#L163

ysemennikov commented 3 months ago

Hi @wangsijie, thank you very much for the response! Unfortunately I can't catch the error, Sentry can't do it too. It simply shows that navigation takes a lot of time (I closed the browser tab after 43 seconds):

image

I will try to find a workaround for this, maybe try with a fresh Nuxt project.

By the way, is it possible to refresh an ID token in Nuxt? It seems like I have to reload the page if it expires, because the Logto Client is only available only on the server side.

wangsijie commented 3 months ago

hi @ysemennikov

  1. Could you please show how you did the redirection? A simple example code in a repo or something like codesandbox would be great.
  2. I'll find a way for error handling
  3. In order to get a new ID token, the user should go through the OIDC sign in flow again, which means redirect to Logto's sign in page through client.signIn()
ysemennikov commented 3 months ago

Hi @wangsijie

  • Could you please show how you did the redirection? A simple example code in a repo or something like codesandbox would be great.

We redirect to sign-in page using the usual NuxtLink. The user has to click this link in order to be redirected.

Here is the codesandbox: https://codesandbox.io/p/devbox/logto-nuxt-bug-pdsww3. There is a SyntaxError in the preview (don't know why), but you should be able to open it locally.

Well, there is only one Nuxt middleware that can influence redirect: middleware/auth.global.ts. But actually it seems to be configured properly, don't think it causes the error.

wangsijie commented 3 months ago

Hi @ysemennikov

I've successfully run the project on my end.

However, I was unable to reproduce the "endless loading" problem you reported. When the token expires, I'm encountering a 500 error with the message "Response.clone: Body has already been consumed." instead of the endless loading behavior.

Upon investigation, I've identified that this 500 error is occurring because the "fetchUserInfo" function is failing. To address this, I'll be preparing a patch to fix the issue. The update will modify the failed request to return null instead of throwing an error, which should resolve the problem. Also, this will make the user in the middleware to be null, and then you can get the authentication state correctly.

wangsijie commented 3 months ago

hi @ysemennikov, a new version of SDK is just released, resolving the above 500 error, could you please try again?

ysemennikov commented 3 months ago

Hi @wangsijie, thank you for the update! I have just updated the package on our side and am testing it right now. Will get back soon!

ysemennikov commented 2 months ago

Hi @wangsijie, sorry for so a long response. I have tested the new version now and unfortunately using organization tokens still resulted in endless loading. However, a couple of days ago we've decided to use only access tokens (with client.getAccessToken method) because of our business logic's requirements. Everything works well now, I haven't noticed any bugs like endless loading.

As far as I know, client.getOrganizationToken uses client.getAccessToken under the hood, so I assume the error has been resolved and we can close the issue for now.

wangsijie commented 2 months ago

Thanks @ysemennikov, I'll do futher invesitigation to find any other possible cause. Feel free to reopen!