next-auth icon indicating copy to clipboard operation
next-auth copied to clipboard

Next-auth: Access token not refreshing when site is left idle

Open AnmolSaini16 opened this issue 3 years ago • 2 comments

Question 💬

So I am using keycloak as my provider where access token lifespan is 5 mins and refresh token is 30 mins. When I am using the application everything is working fine: Access token refreshes after every 5 mins whch in turn refreshes the refresh token each time. The problem arises when I switch the to another tab or do some other stuff leaving the application tab, the access token stops getting called in the background, so it doesn't refresh the refresh token and throws error which in turn logs out the user when the refresh token expires. Which shouldn't be the case, if user comes back after more than 30 mins he/she should be logged in till the session is not expired.

In [...nextauth].ts


const refreshAccessToken = async (token: JWT) => {
    try {
        if (Date.now() > token.refreshTokenExpired) {
            console.log('Error thrown');
            throw Error;
        }
        const details = {
            client_id: process.env.KEYCLOAK_AUTH_CLIENT_ID,
            client_secret: process.env.KEYCLOAK_AUTH_CLIENT_SECRET,
            grant_type: 'refresh_token',
            refresh_token: token.refreshToken
        };
        const formBody: string[] = [];
        Object.entries(details).forEach(([key, value]: [string, any]) => {
            const encodedKey = encodeURIComponent(key);
            const encodedValue = encodeURIComponent(value);
            formBody.push(encodedKey + '=' + encodedValue);
        });
        const formData = formBody.join('&');
        const url = process.env.KEYCLOAK_AUTH_TOKEN_URL || '';
        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
            },
            body: formData
        });
        const refreshedTokens = await response.json();
        if (!response.ok) throw refreshedTokens;
        return {
            ...token,
            accessToken: refreshedTokens.access_token,
            accessTokenExpired: Date.now() + refreshedTokens.expires_in * 1000,
            refreshTokenExpired: Date.now() + refreshedTokens.refresh_expires_in * 1000,
            refreshToken: refreshedTokens.refresh_token ?? token.refreshToken
        };
    } catch (error) {
        console.log('Token expired');
        console.log(error);
        return {
            ...token,
            error: 'RefreshAccessTokenError'
        };
    }
};

export const authOptions: NextAuthOptions = {
    providers: [
        KeycloakProvider({
            clientId: process.env.KEYCLOAK_AUTH_CLIENT_ID || '',
            clientSecret: process.env.KEYCLOAK_AUTH_CLIENT_SECRET || '',
            issuer: process.env.KEYCLOAK_AUTH_ISSUER,
            requestTokenUrl: process.env.KEYCLOAK_AUTH_TOKEN_URL,
        })
    ],
    secret: process.env.NEXTAUTH_SECRET,
    pages: {
        signIn: '/auth/signin',
        signOut: '/auth/signout',
        error: '/auth/error'
    },
    callbacks: {
        async jwt({ token, user, account }) {
            if (account && user) {
                // Add access_token, refresh_token and expirations to the token right after signin
                token.accessToken = account.access_token;
                token.refreshToken = account.refresh_token;
                token.accessTokenExpired = account.expires_at * 1000;
                token.refreshTokenExpired = Date.now() + account.refresh_expires_in * 1000;
                token.user = user;
                return token;
            }

            // Return previous token if the access token has not expired yet
            if (Date.now() < token.accessTokenExpired) {
                return token;
            }

            // Access token has expired, try to update it
            return refreshAccessToken(token);
        },
        async session({ session, token }) {
            session.user = token.user;
            session.accessToken = token.accessToken;
            session.error = token.error;
            return session;
        }
    }
};
export default NextAuth(authOptions);

In _app.tsx

 <SessionProvider
                           session={session}
                           refetchInterval={4 * 60} // Refetch session after every 4 mins so that session doesn't expire.
                           basePath={process.env.NEXTAUTH_URL}
  >

How to reproduce ☕️

Use Keycloak provider in next-auth provider, set the access token and refresh token life span. Leave the website and move to another tab. Access token will not refresh itself in background.

Contributing 🙌🏽

Yes, I am willing to help answer this question in a PR

AnmolSaini16 avatar Sep 02 '22 11:09 AnmolSaini16

@balazsorban44 Please look into it.

AnmolSaini16 avatar Sep 05 '22 07:09 AnmolSaini16

We're working on the fix for this issue in https://github.com/nextauthjs/next-auth/pull/4940

ThangHuuVu avatar Sep 12 '22 14:09 ThangHuuVu

It looks like this issue did not receive any activity for 60 days. It will be closed in 7 days if no further activity occurs. If you think your issue is still relevant, commenting will keep it open. Thanks!

stale[bot] avatar Nov 12 '22 01:11 stale[bot]

To keep things tidy, we are closing this issue for now. If you think your issue is still relevant, leave a comment and we might reopen it. Thanks!

stale[bot] avatar Nov 22 '22 17:11 stale[bot]

Does anybody have a new update on this issue?

peterburgs avatar Nov 23 '22 11:11 peterburgs

I'm having the same problem with this. Anyone has any update on this issue ?

HeliosAiden avatar Dec 28 '23 16:12 HeliosAiden

Still an open issue in v4.24.5.

Is this already fixed in another Issue?

swag-overflow avatar May 27 '24 12:05 swag-overflow