'NextJS and NextAuth session user object getting lost due to [...nextauth.ts] getting triggered to be recompiled

I am learning NextJS and NextAuth and have implemented a Credentials sign in with my own login page and it is working where the session object contains my user model (at the moment contains everything including the password but obviously it won't stay like that).

I can refresh the page and the session is maintained, however if I leave for a minute or two and then refresh the user object in my session becomes the default even though my session is not supposed to expire until next month.

Below is my [...nextauth.tsx] file

import NextAuth, {NextAuthOptions} from 'next-auth'
import Providers from 'next-auth/providers'
import { PrismaClient } from '@prisma/client'
import {session} from "next-auth/client";

let userAccount = null;

const prisma = new PrismaClient();

const providers : NextAuthOptions = {
    site: process.env.NEXTAUTH_URL,
    cookie: {
        secure: process.env.NODE_ENV && process.env.NODE_ENV === 'production',
    },
    redirect: false,
    providers: [
        Providers.Credentials({
            id: 'credentials',
            name: "Login",
            async authorize(credentials : any) {
                const user = await prisma.users.findFirst({
                    where: {
                        email: credentials.email,
                        password: credentials.password
                    }
                });

                if (user !== null)
                {
                    userAccount = user;
                    return user;
                }
                else {
                    return null;
                }
            }
        })
    ],
    callbacks: {
        async signIn(user, account, profile) {
            console.log("Sign in call back");
            console.log("User Is");
            console.log(user);
            if (typeof user.userId !== typeof undefined)
            {
                if (user.isActive === '1')
                {
                    console.log("User credentials accepted")
                    return user;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                console.log("User id was not found so rejecting signin")
                return false;
            }
        },
        async session(session, token) {
            //session.accessToken = token.accessToken;
            if (userAccount !== null)
            {
                session.user = userAccount;
            }
            console.log("session callback returning");
            console.log(session);
            return session;
        },
        /*async jwt(token, user, account, profile, isNewUser) {
            console.log("JWT User");
            console.log(user);
            if (user) {
                token.accessToken = user.token;
            }
            return token;
        }*/
    }
}

const lookupUserInDb = async (email, password) => {
    const prisma = new PrismaClient()
    console.log("Email: " + email + " Password: " + password)
    const user = await prisma.users.findFirst({
        where: {
            email: email,
            password: password
        }
    });
    console.log("Got user");
    console.log(user);
    return user;
}

export default (req, res) => NextAuth(req, res, providers).

I trigger the signin from my custom form as below

signIn("credentials", {
            email, password, callbackUrl: `${window.location.origin}/admin/dashboard`, redirect: false }
        ).then(function(result){
            if (result.error !== null)
            {
                if (result.status === 401)
                {
                    setLoginError("Your username/password combination was incorrect. Please try again");
                }
                else
                {
                    setLoginError(result.error);
                }
            }
            else
            {
                router.push(result.url);
            }
            console.log("Sign in response");
            console.log(result);
        });

Signin is imported from next-auth/client

My _app.js is below:

export default function Blog({Component, pageProps}) {
    return (
        <Provider session={pageProps.session}>
            <Component className='w-full h-full' {...pageProps} />
        </Provider>
    )

}

Then the page where it redirects to after signin has the below: (Not sure if this actually does anything other than fetch the active session so I can reference it from the frontend)

const [session, loading] = useSession()

When I signin, the callback for session in [...nextauth.tsx] returns the following:

session callback returning
{
  user: {
    userId: 1,
    registeredAt: 2021-04-21T20:25:32.478Z,
    firstName: 'Some',
    lastName: 'User',
    email: 'someone@example',
    password: 'password',
    isActive: '1'
  },
  expires: '2021-05-23T17:49:22.575Z'
}

Then for some reason the terminal that is running npm run dev from inside PhpStorm then outputs

event - build page: /api/auth/[...nextauth]
wait  - compiling...
event - compiled successfully

But I didn't change anything, and even if I did, surely me making a change to the app, shouldn't trigger the session to be removed, yet straight after this, the session callback then returns the following:

session callback returning
{
  user: { name: null, email: '[email protected]', image: null },
  expires: '2021-05-23T17:49:24.840Z'
}

So I'm a little confused, it seems like that maybe my code is working, but maybe PhpStorm is triggering recompile and then the session is cleared, but as I said above, surely making a change and the recompiled version shouldn't trigger the session to be modified.

Update

I have done a test where I did a build and started is a production version, and I can refresh the page as much as I want and the session is maintained, so I've proved that my code works fine. So it looks like it's something do with PhpStorm determining something has changed and doing a recompile even though nothing has changed.



Solution 1:[1]

I've finally found the solution.

To the provider options I added the following:

session: {
        jwt: true,
        maxAge: 30 * 24 * 60 * 60

    }

The session callback I changed to be the following:

async session(session, token) {
    //session.accessToken = token.accessToken;
    console.log("Session token");
    console.log(token);
    if (userAccount !== null)
    {
        session.user = userAccount;
    }
    else if (typeof token !== typeof undefined)
    {
        session.token = token;
    }
    console.log("session callback returning");
    console.log(session);
    return session;
}

The jwt callback was as follows:

async jwt(token, user, account, profile, isNewUser) {
    console.log("JWT Token User");
    console.log(token.user);
    if (typeof user !== typeof undefined)
    {
         token.user = user;
    }
    return token;
}

Basically I was misunderstanding that I needed to use the jwt callback and on the first call to this callback, the user model that is set from the signIn callback is used so I can add that to the token which can then be added to the session in the session callback.

The issue in subsequent requests to jwt, the user parameter doesn't get set so I was then setting the token user object to be undefined which is why my session was getting blanked.

I don't understand though why I didn't seem to get the behaviour when running it as a production build.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Boardy