next-auth: Can't use auth() to get values from jwt() callback in v5

Environment

next: 14.0.2 => 14.0.2 
next-auth: ^5.0.0-beta.3 => 5.0.0-beta.3 
react: ^18 => 18.2.0 

Reproduction URL

https://github.com/Celsiusss/nextauth-demo

Describe the issue

In the documentation, it says that:

When using auth(), the session() callback is ignored. auth() will expose anything returned from the jwt() callback or if using a “database” strategy, from the User. This is because the session() callback was designed to protect you from exposing sensitive information to the client, but when using auth() you are always on the server.

I tried to test this, but could not get values from the jwt() callback to be returned by auth(). It seems like the opposite of what this text says is true. Values from the session() callback is being returned, instead of jwt(). Not quite sure what the intended behavior really is, but I would love to access encrypted token values using auth().

How to reproduce

I have tried to create a minimal example in the Github link.

Here I am using auth() in a server component and in a route handler, to demonstrate it in two different ways. page.tsx shows it being used in a server component. api/route.ts shows it being used in a route handler. client.tsx client component for signing in and using the route handler auth.ts registers an oidc provider and defines the jwt() callback.

Expected behavior

I expected to be able to use auth() to retrieve values from the jwt() callback in server side components.

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Reactions: 29
  • Comments: 35 (6 by maintainers)

Most upvoted comments

Thanks to everyone for their help. For some reason I had to add more type checks to make it work, in case anyone wants a fully working auth.config.ts example …

import type { NextAuthConfig } from 'next-auth';
import { DefaultSession } from 'next-auth';
import { User as NextUser } from 'next-auth';

declare module "next-auth" {
  interface Session {
    user: {
      id: string
    } & DefaultSession["user"]
  }
}

export const authConfig = {
  pages: {
    signIn: '/login',
  },
  session: {
    strategy: "jwt",
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }: { auth: any, request: { nextUrl: any } }) {
     // fill in the blank here
    },
    jwt({ token, user }) {
      if (user) token.user = user;
      return token;
    },
    session(sessionArgs) {
     // token only exists when the strategy is jwt and not database, so sessionArgs here will be { session, token }
     // with a database strategy it would be { session, user } 
     if ("token" in sessionArgs) {
        let session = sessionArgs.session;
        if ("user" in sessionArgs.token) {
          const tokenUser = sessionArgs.token.user as NextUser;
          if (tokenUser.id) {
            session.user.id = tokenUser.id;
            return session;
          }
        }
     }
     return sessionArgs.session;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

Just to emphasize this again: I personally do not only want the raw unencrypted token, but have a refreshed one. In other words: I’m looking for a method to get the content of the jwt() callback including a Set-Cookie header in case the jwt() content changed (e.g. refresh token rotation occured). Something like unstable_getServerSession(), but for the jwt() content instead of session()

If I understand the documentation correctly, getToken() does indeed get the raw token, but it’s the token from the cookie, which is potentially stale

whats is resolver ? 🚩

image

Just to add, I am not quite able to get the getToken function from @auth/core/jwt to work. Even tho I am passing the same parmeters to the function that get passed by the library internally; even is this worked I don’t think it’d be a good idea to mimic the internals. Maybe @balazsorban44 or someone from the dev team could advice in this case on how to get the raw JWT token?

I managed to get the getToken(…) to return the JWT with the following Route Handler: image

Intentionally didn’t send in a salt param, typescript does not approve.

But as @czymstef mentions, with this the token is extracted from the cookie, which makes the token potentially stale.

EDIT — Managed to get the JWT from a Server Action as well. This is from a Next.js app.

import { getToken } from "@auth/core/jwt";
import { cookies } from 'next/headers'

export async function action() {
    "use server"

    const all_cookies = cookies().getAll();
    const headers = new Headers();

    all_cookies.forEach((cookie) => {
        headers.set("cookie", `${cookie.name}=${cookie.value};`)
    });

    const req = {
        headers
    };

    const secureCookie = process.env.NODE_ENV === "production";
    const cookieName = secureCookie
        ? "__Secure-authjs.session-token"
        : "authjs.session-token"

    const jwt = await getToken({ req, secret: process.env.AUTH_SECRET!, secureCookie, salt: cookieName });
    console.log(jwt);
}

The documentation has changed. The bit that I originally quoted is now gone. (b5d5f20)

The commit references another issue (#9329) that was created after this describing the same problem.

Quoting a comment by @balazsorban44 made on that issue:

we reverted the ignoring part and will rather introduce the change via a flag in https://github.com/nextauthjs/next-auth/pull/9702 so the behavior is more consistent/less breaking for now. Will update the migration guide shortly

https://github.com/nextauthjs/next-auth/issues/9329#issuecomment-1909255054

Seems like auth() will only use the session() callback. A new way of suing auth() with AuthData is in the works, and may solve this.

Would have appreciated some better communication seeing as this issue has gathered a lot of traction, but at the same time many people commenting here is not even commenting about the original issue that I described, so I can see due to the discussions that emerged here it may not be clear what this issue really was about.

For me that’s not true I doesn’t have the jwt properties in my session object image

Is any of you guys by any chance using the drizzle-adapter and have the extended session object working?

Just ran into this as well after upgrading from a previous beta version. This is a pretty critical bug.

@vineetkia You might find this page helpful buried deep within the docs - Module Augmentation. Essentially you declare a module for ‘next-auth’ (or any other module you might want to override and then redeclare the interface’s with new properties, and then anywhere in your code that you use somethign that has Session, it will have all the new properties you are planning to include, so you don’t have to fight typescript.

declare module "next-auth" {
  interface Session {
    address: string
  }
}

It will merge in new properties. If you want to modify a nested object within you need to tweak slightly:

declare module "next-auth" {
  interface Session {
    user: {
      address: string
    } & DefaultSession["user"]
  }
}

You can just inport DefaultSession from the next-auth library.

You can also augment the type declaration to get rid of the type error in a cleaner way.

Hey, I’m facing the same issue. However, you can “access” the token object even though there’s a type error

    async session({ session, token }) {
      session.user.id = token.id;
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
      }
      return token;
    },

Just as a note, the type error with using token in the session callback is due to the typing where it will either have a token (if using jwt) or a user (if using a database); you can get around the type error somewhat by using the in operator narrowing:

    session(sessionArgs) {
         // token only exists when the strategy is jwt and not database, so sessionArgs here will be { session, token }
        // with a database strategy it would be { session, user } 
        if ("token" in sessionArgs) {
            return {
                 ...sessionArgs.session,
                user: {
                        ...sessionArgs.session.user,
                        roles: sessionArgs.token.roles, // something you are bringing in from your hwt function return via the token
                },
            };
        }
        return sessionArgs.session;
     }

If you need more info from the OIDC provider you can always get it by modifying the session object through the jwt and session callback. Just get whatever you need from the account parameter that gets passed to the jwt callback, then that object gets passed down to session and whatever you return from that you can get through auth. I do it in my app:

  session: ({ session, token }) => {
      // token has the object returned from the jwt callback
     // here you can modify the session object as you please
      session.user = {...session.user, id: token.user.id}
      return session
    },
    jwt: async ({ account, user }) => {
      // account has the user info from the OIDC provider
      if (account) {
        return {
          user: {
              ...user,
             // add whatever property from account
             id: account.id
          },
        }
        // @ts-expect-error
      }
      return user
      }
    },

You can then use module augmentation to get type completion.

@alessandrojcm Mind sharing how you could get the token server side in v4? I thought that this was not possible, too (with refresh token rotation and all).

@judewang If you don’t export the handlers, then the callback endpoint does not exist, too, which makes logging in impossible, doesn’t it? But we could only export the required handlers and just don’t export the session one, or as @alessandrojcm suggested: Catch the call to /session and strip it. But who says that authjs won’t add a method at a later stage which uses server actions?

This is just not as the library is intended to be used; session() is for client side data. I don’t think you should have to jump through those hoops to just get the (possibly refreshed, including new Set-Cookie headers) value from jwt() on server side.

There is v4’s getToken, though now that I think of it I am not entirely sure if that does execute session; you might be right.

And as per your second point, yes you are right one should not rely on patching session since it might either break in the future or we might be leaking the token somewhere else. IMO there should be a way of getting the raw token server-side as we might need it, for examplet to authenticate with 3rd party APIs if necessary.

So is there no way at the moment to get the raw token server-side? Seems pretty limiting

I also met the issue and figured out that the migration guide of this part was a little misleading. After tracing the source code, I found that the auth returned by NextAuth was just a getSession call with auth config. According to the documentation, both of the returned values of auth and getSession are Session and Session is the return value of session() callback, so the return value of auth is the return value of session() callback, not the return value of jwt() callback. The return value of jwt() callback will be encoded and set within server side cookie.

If we want auth to contain the returned value of jwt() callback, we can do this within session() callback.

callbacks: {
  session({ session, token }) {
    // token is the returned value of `jwt()`
    return { ...session, ...token }
    // Can just return token if you want.
  }
}