nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
22.87k stars 3.08k forks source link

Can't use auth() to get values from jwt() callback in v5 #9122

Closed Celsiusss closed 4 months ago

Celsiusss commented 7 months ago

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.

czymstef commented 7 months ago

I've encountered the same issue, and it would be fantastic if it worked as described in the documentation. I aim to avoid exposing the access token to the client.

As far as I understand, there isn't a way to obtain the up-to-date JWT in server calls. Although there's a getToken method, it only retrieves the JWT from the request, which might require updating (e.g., refresh token rotation). Getting the updated JWT directly from auth() would resolve this.

mwawrusch commented 6 months ago

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

judewang commented 6 months ago

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.
  }
}
czymstef commented 6 months ago

@judewang The problem with this approach is that the unencrypted token is exposed to the client.

judewang commented 6 months ago

@czymstef I supposed we can avoid exposing the token without using useSession, was I wrong? Since we are always on the server, are there any differences that the Session returned from auth is somehow by session() or by jwt() ?

BTW the encrypted token is within cookie.

czymstef commented 6 months ago

As far as I know, there is still the /api/auth/session endpoint (used by useSession()), which is just one GET-request away. I'm sure that there is a way to "disable" this endpoint (e.g. by defining a no op api route with the same name), but I'd not bet that there is no other way to somehow get the token in a compromised client.

judewang commented 6 months ago

I got a 404 error when trying to access /api/auth/session. CleanShot 2023-11-27 at 21 43 56@2x

The values within cookie were also encrypted. CleanShot 2023-11-27 at 21 45 37@2x

alessandrojcm commented 6 months ago

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

czymstef commented 6 months ago

@judewang go to https://next-auth-example.vercel.app/, which should run on next-auth v5 beta. Login using github, then in the developer console write: await fetch("https://next-auth-example.vercel.app/api/auth/session").then(res => res.json()) You should get the result from the session() callback, which is where the token would be leaked if I'd include it in the session.

@alessandrojcm I think there is a way using getToken(), but as far as I know it won't call the jwt() callback and thus does not run stuff like refresh token rotation.

judewang commented 6 months ago

@czymstef I tried https://next-auth-example.vercel.app and also got my token printed within the console. However when doing the same thing on my site, I still got a 404 error. I noticed that I didn't export the handlers to any api routes, and that was what I found the demo page done, so maybe that caught the leak.

https://github.com/nextauthjs/next-auth/blob/main/apps/examples/nextjs/app/api/auth/%5B...nextauth%5D/route.ts

alessandrojcm commented 6 months ago

@judewang go to https://next-auth-example.vercel.app/, which should run on next-auth v5 beta. Login using github, then in the developer console write: await fetch("https://next-auth-example.vercel.app/api/auth/session").then(res => res.json()) You should get the result from the session() callback, which is where the token would be leaked if I'd include it in the session.

@alessandrojcm I think there is a way using getToken(), but as far as I know it won't call the jwt() callback and thus does not run stuff like refresh token rotation.

Yeah I noticed getToken only decodes the token from the cookies, that may or not be enough depending on your use case; still I believe the library should offer a native way or getting the token as it was in v4. Alternatively one could export the function that does token rotation an call that beofore of getToken, although that is not ideal.

As per adding the token to the session callback, I think that could be done and, to avoid leaking the token, one could catcth the call to api/auth/session within the route handler and just strip the token from the response. Although you'd need to be very careful where you call getSession to avoid leaking the token.

czymstef commented 6 months ago

@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.

alessandrojcm commented 6 months ago

@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.

alessandrojcm commented 6 months ago

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?

czymstef commented 6 months ago

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

G4RDS commented 5 months ago

I'm experiencing the same issue. Has there been any update or resolution?

I'm attempting to retrieve user IDs using auth() in Server Components, but returned user object includes only name, email, and image.

const session = await auth();
console.log(session.user) // { image: '...', email: '...', name: '...' }

I ran through the code and figured out that this segment does nothing:

https://github.com/nextauthjs/next-auth/blob/d7a116558700785880ecb03658b7de7c98bcf4bf/packages/next-auth/src/lib/index.ts#L76-L83

Because the supplied args[0].session has hard-coded user which only contains them.

https://github.com/nextauthjs/next-auth/blob/d7a116558700785880ecb03658b7de7c98bcf4bf/packages/core/src/lib/actions/session.ts#L52-L59

eessadrati commented 5 months ago

@G4RDS I have the same issue, did you find any solution?

statusunknown418 commented 4 months ago

@G4RDS does doing the module augmentation had any positive impact on solving the issue? or did you find a solution?

alessandrojcm commented 4 months ago

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.

statusunknown418 commented 4 months ago

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

snettah commented 4 months ago

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

image
felicksLindgren commented 4 months ago

Currently facing the same issue, does not feel safe including 3rd party token in the session object.

felicksLindgren commented 4 months ago

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);
}
devsmartproject commented 4 months ago

whats is resolver ? 🚩

image

obnol commented 4 months ago

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;
    },
ARJohnsonKwik commented 4 months ago

Has anyone found a way to access the JWT token server side / api routes? Upgrading to v5 really seems like a down grade so far.

felicksLindgren commented 4 months ago

Has anyone found a way to access the JWT token server side / api routes? Upgrading to v5 really seems like a down grade so far.

Not tried a API route, but managed to access the JWT via getToken(...) in a Next.js Route Handler and a Server Action (https://github.com/nextauthjs/next-auth/issues/9122#issuecomment-1898644981).

All it does is decode the cookie with your AUTH_SECRET.

It's a work around for sure but as long as you have the Request Headers available you should be able to use this method if thinking.

xiavn commented 4 months ago

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;
     }
alessandrojcm commented 4 months ago

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

xiavn commented 4 months ago

Yes good point! I was augmenting my session shape to avoid errors, didn't think of augmenting the session function args too.

vineetkia commented 4 months ago

How did you augment your session shape to avoid errors @xiavn , can you provide the code please?

xiavn commented 4 months ago

@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.

lukasb commented 4 months ago

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;
vineetkia commented 4 months ago

I switched back to "5.0.0-beta.4" @lukasb , and it worked fine for me. in 5.0.0-beta.5 they had this breaking change. I guess until they go under stable release the 5.0.0-beta.4 works fine for session({ session, token })

Celsiusss commented 4 months ago

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.

gidsola commented 1 month ago

currently migrating to v5. The guides are EXTREMELY useless. There are NO useable examples. I'm also pretty sure not everyone knows how to interface in order to achieve a custom dataset.

almost ranted... anyways, bottom line. The documentation is garbage and I've spent 2 days migrating something that should have taken hours.

Examples folks, ones that work. Ones that are current. P.S. Migration guide indicates use of middleware, your API says its deprecated and using it creates errors with bcrypt.(another issue)

sorry, nothing nice to say here, I guess I did rant..

olems commented 1 week ago

Faced the same issue and the struggles with documentation about this use case. For me the only workable solution was to use getToken(), even though the docs say it is not recommended.