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

jwt rotation not working properly after browser close and open #498

Closed
quroom opened this issue Jul 27, 2023 · 7 comments
Closed

jwt rotation not working properly after browser close and open #498

quroom opened this issue Jul 27, 2023 · 7 comments
Assignees
Labels
documentation A change to the documentation p3 Minor issue provider-authjs An issue with the authjs provider

Comments

@quroom
Copy link

quroom commented Jul 27, 2023

Environment

Windows 10
Nuxi 3.6.5
Nuxt 3.6.5 with Nitro 2.4.1
@sidebase/nuxt-auth 0.4.4
next-auth 4.22.3

Reproduction

No response

Describe the bug

This is own my jwt refresh code.
This code works well until browser close.

import { NuxtAuthHandler } from "#auth";
import GoogleProvider from "next-auth/providers/google";
import NaverProvider from "next-auth/providers/naver";
import { JwtUtils } from "@/constants/utils";
const config = useRuntimeConfig();
export default NuxtAuthHandler({
  secret: process.env.NUXT_SECRET,
  pages: {
    // Change the default behavior to use `/login` as the path for the sign-in page
    signIn: "/login",
  },
  providers: [
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    NaverProvider.default({
      clientId: process.env.NAVER_CLIENT_ID,
      clientSecret: process.env.NAVER_CLIENT_SECRET,
    }),
  ],
  callbacks: {
    async jwt({ token, user, account, profile }) {
      console.log(
        `[current] access: ${token.accessToken}, refresh: ${token.refreshToken}`
      );
      try {
        if (user && account) {
          if (account.provider === "google" || account.provider === "naver") {
            const response = await $fetch(
              `${config.public.apiBase}/dj-rest-auth/${account.provider}/`,
              {
                method: "POST",
                body: {
                  access_token: `${
                    account.provider === "google"
                      ? account.id_token
                      : account.access_token
                  }`,
                },
              }
            );
            const { access, refresh } = response;
            token = {
              ...token,
              accessToken: access,
              refreshToken: refresh,
            };
          }
        } else if (!token.accessToken || !token.refreshToken) {
          return {};
        } else if (JwtUtils.isJwtExpired(token.accessToken)) {
          const response = await $fetch(
            `${config.public.apiBase}/dj-rest-auth/token/refresh/`,
            {
              method: "POST",
              body: {
                refresh: token.refreshToken,
              },
            }
          );
          const { access, refresh } = response;
          console.log(`[new] access: ${access}, refresh: ${refresh}`);
          token = {
            ...token,
            accessToken: access,
            refreshToken: refresh,
          };
          return token;
        }
      } catch (error) {
        if (error.data) {
          console.debug(token.email, error.data);
        } else {
          console.debug(token.email, error);
        }
        return {};
      }
      return token;
    },
    async session({ session, token }) {
      if (token.accessToken) {
        session.accessToken = token.accessToken;
      } else {
        session = {};
      }
      return session;
    },
  },
  events: {
    async signOut({ token }) {
      await $fetch(`${config.public.apiBase}/dj-rest-auth/logout/`, {
        method: "POST",
        body: {
          refresh: token.refreshToken,
        },
      });
    },
  },
});

But in some scenario, user can close browser and after 12 hours like enough expiration access token,
user can open browser again. Then the code refresh access_token and refresh_token automatically.
And then in jwt callback function, returns new access token and refresh token.
But somehow, the token is not updated correctly.
I don't know how I can fix it correctly but I can only figure the cookie is not updated.

frontend\node_modules@sidebase\nuxt-auth\dist\runtime\server\services\nuxtAuthHandler.mjs#125

This code is always runned when jwt callback run like refresh tab , open browser.
So it works well with refreshed tab while browser running.
But it's not working with two conditions.

  1. browser closed
  2. token expired

Then as soon as browser open, the token is updated. And session-token cookie should be updated too matched new token. But somehow even if setCookie code was run, but cookie is still same as previous session-token cookie.
Maybe.. there might be bug with nuxt initialize code with cookie and update cookie after jwt callback logic.

Additional context

업데이트되야할 쿠키토큰
This cookie should be updated to browser cookie

실제 쿠키토큰
But after code run, cookie is still same as before.

Logs

No response

@quroom quroom added the bug label Jul 27, 2023
@quroom
Copy link
Author

quroom commented Jul 28, 2023

I am not sure what the problem is.. but I modified directus code.
Now it works properly as what I am expecting.

I left my code for someone who faces same problem.

import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import NaverProvider from "next-auth/providers/naver";
import { NuxtAuthHandler } from "#auth";
const config = useRuntimeConfig();

// Ref.https://sidebase.io/nuxt-auth/recipes/directus
/**
 * Takes a token, and returns a new token with updated
 * `accessToken` and `accessTokenExpires`. If an error occurs,
 * returns the old token and an error property
 */
async function refreshAccessToken(refreshToken: {
  accessToken: string;
  accessTokenExpires: string;
  refreshToken: string;
}) {
  try {
    console.warn("trying to post to refresh token");
    const refreshedTokens = await $fetch<{
      data: {
        access: string;
        access_expiration: string;
        refresh: string;
      };
    } | null>(`${config.public.apiBase}/dj-rest-auth/token/refresh/`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: {
        refresh: refreshToken.refreshToken,
        mode: "json",
      },
    });
    if (!refreshedTokens || !refreshedTokens.access) {
      console.warn("No refreshed tokens");
      throw refreshedTokens;
    }
    console.warn("Refreshed tokens successfully");
    return {
      ...refreshToken,
      accessToken: refreshedTokens.access,
      accessTokenExpires: new Date(refreshedTokens.access_expiration),
      refreshToken: refreshedTokens.refresh,
    };
  } catch (error) {
    console.warn("Error refreshing token", error);
    return {
      error: "RefreshAccessTokenError",
    };
  }
}

export default NuxtAuthHandler({
  // secret needed to run nuxt-auth in production mode (used to encrypt data)
  secret: process.env.NUXT_SECRET,
  providers: [
    GoogleProvider.default({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    NaverProvider.default({
      clientId: process.env.NAVER_CLIENT_ID,
      clientSecret: process.env.NAVER_CLIENT_SECRET,
    }),
    // @ts-expect-error You need to use .default here for it to work during SSR. May be fixed via Vite at some point
    CredentialsProvider.default({
      // The name to display on the sign in form (e.g. 'Sign in with...')
      name: "Credentials",
      // The credentials is used to generate a suitable form on the sign in page.
      // You can specify whatever fields you are expecting to be submitted.
      // e.g. domain, username, password, 2FA token, etc.
      // You can pass any HTML attribute to the <input> tag through the object.
      credentials: {
        id: { label: "id", type: "text" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials: any) {
        // You need to provide your own logic here that takes the credentials
        // submitted and returns either a object representing a user or value
        // that is false/null if the credentials are invalid.
        // NOTE: THE BELOW LOGIC IS NOT SAFE OR PROPER FOR AUTHENTICATION!
        try {
          const payload = {
            username: credentials.id,
            password: credentials.password,
          };
          const userTokens = await $fetch<{
            data: {
              access: string;
              access_expiration: number;
              refresh: string;
            };
          } | null>(`${config.public.apiBase}/dj-rest-auth/login/`, {
            method: "POST",
            body: payload,
            headers: {
              "Content-Type": "application/json",
              "Accept-Language": "en-US",
            },
          });
          const userDetails = await $fetch<{
            data: {
              pk: string;
              username: string;
              email: string;
            };
          } | null>(`${config.public.apiBase}/dj-rest-auth/user`, {
            method: "GET",
            headers: {
              "Content-Type": "application/json",
              "Accept-Language": "en-US",
              Authorization: `Bearer ${userTokens?.access}`,
            },
          });
          if (!userTokens || !userDetails) {
            throw createError({
              statusCode: 500,
              statusMessage: "Next auth failed",
            });
          }
          const user = {
            pk: userDetails.pk,
            email: userDetails.email,
            accessToken: userTokens.access,
            accessTokenExpires: new Date(userTokens.access_expiration),
            refreshToken: userTokens.refresh,
          };
          const allowedRoles = [
            "53ed3a6a-b236-49aa-be72-f26e6e4857a0",
            "d9b59a92-e85d-43e2-8062-7a1242a8fce6",
          ];
          // Only allow admins and sales
          // if (!allowedRoles.includes(user.role)) {
          //   throw createError({
          //     statusCode: 403,
          //     statusMessage: 'Not allowed'
          //   })
          // }
          return user;
        } catch (error) {
          console.warn("Error logging in", error);
          return null;
        }
      },
    }),
  ],
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user, account }) {
      if (account && user) {
        // console.warn('JWT callback', { token, user, account })
        if (account.provider === "google" || account.provider === "naver") {
          const response = await $fetch(
            `${config.public.apiBase}/dj-rest-auth/${account.provider}/`,
            {
              method: "POST",
              body: {
                access_token: `${
                  account.provider === "google"
                    ? account.id_token
                    : account.access_token
                }`,
              },
            }
          );
          const { access, refresh, access_expiration } = response;
          token = {
            ...token,
            accessToken: access,
            accessTokenExpires: new Date(access_expiration),
            refreshToken: refresh,
          };
        }
        return {
          ...token,
          ...user,
        };
      }
      // Handle token refresh before it expires of 15 minutes
      if (
        token.accessTokenExpires &&
        Date.now() > new Date(token.accessTokenExpires)
      ) {
        console.log(`current: ${token.refreshToken}`);
        console.warn("Token is expired. Getting a new");

        const newToken = await refreshAccessToken(token);
        console.log(`newToken: ${newToken.refreshToken}`);
        return newToken;
      }
      return token;
    },
    async session({ session, token }) {
      if (token.error) {
        return {};
      } else {
        session.user = {
          ...session.user,
          ...token,
        };
      }
      return session;
    },
  },
});

Don't set env variable.
This env variable makes problem too.
NEXTAUTH_URL=http://127.0.0.1:3000
I don't understand what the problem was.
But I found workaround so I closed issue.

@quroom quroom closed this as completed Jul 28, 2023
@quroom quroom reopened this Jul 31, 2023
@quroom
Copy link
Author

quroom commented Jul 31, 2023

It's not solved..
It sometimes occurs , it makes me crazy..
There are more developes face same issues in next-auth repo.
I guess I need to give up using nuxt-auth based on nuxt-auth.
The problem is not from nuxt-auth, it's more related to next-auth.

And it's an old issue discussed many times in next-auth repo.
nextauthjs/next-auth#1455
nextauthjs/next-auth#2071
nextauthjs/next-auth#2129
nextauthjs/next-auth#3941
All of them are related issue. but there isn't realy solution.
Maybe some comments can be workaround but not real solution.

I hope someday it will be solved. Then auth package can be more valuable

@itsmeny
Copy link

itsmeny commented Dec 13, 2023

I also face this issue....

@KhaledAlMana
Copy link

same problem

@AlejandroAkbal
Copy link

Same issue

@zoey-kaiser zoey-kaiser removed the bug label Feb 23, 2024
@zoey-kaiser
Copy link
Member

Hmm, interesting issue. As you pointed out it seems to be more related to authjs, then our module.

I have used refresh tokens inside the JWT countless times before. I would follow these steps to continue with this issue:

  • Add a refresh provider example for authjs
  • See if applying our refresh logic fixes your issue
  • continue investing

However, as you already pointed our numerous issues in the nextauth repo, it may not be the fix. However, we are also now beginning the migration to authjs, which may fix this issue. Keep an eye on #673 for updates on this!

@zoey-kaiser zoey-kaiser added documentation A change to the documentation p3 Minor issue provider-authjs An issue with the authjs provider labels Feb 23, 2024
@zoey-kaiser zoey-kaiser self-assigned this Feb 23, 2024
@zoey-kaiser
Copy link
Member

Closed due to inactivity.

@zoey-kaiser zoey-kaiser closed this as not planned Won't fix, can't repro, duplicate, stale Jul 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation A change to the documentation p3 Minor issue provider-authjs An issue with the authjs provider
Projects
None yet
Development

No branches or pull requests

5 participants