27

I have weird problem and I don't know where the problem is. I use next-auth library to make authentication system in my Next.js app.

Everything is OK - I can sign in by checking if there is account in google firebase with submitted credentials, session is being created properly, but when the authorize callback is initialized I pass data received from google firestore after correct sign in. User object contains whole data and it's passed forward.

Then, when I want to read data from session, some data I passed before is missing.

Code:

/pages/api/auth/[...nextauth.js]

export default (req, res) => 
    NextAuth(req, res, {
        providers: [
            Providers.Credentials({
                name: 'Credentials',
                credentials: {
                    phone: { label: "Phone number", type: "text" },
                    password: { label: "Password", type: "password" }
                },
                authorize: async (loginData) => {
                    const { csrfToken, phone, password } = loginData;
                    
                    //  checking if there is account with these credentials
                    let res = await login({
                        phone, 
                        password: sha1(md5(password)).toString()
                    })

                    //  200 = OK
                    if(res.status == 200){
                        //  collect account data
                        const user = {
                            phone,
                            ...res.data.info
                        }

                        //  user object is created correctly, whole data is stored there
                        console.log('received account data from firestore', user)
                        return Promise.resolve(user);
                    }
                    else {
                        //  wrong credentials
                        return Promise.resolve(null);
                    }
                }
            })
        ],
        callbacks: {
            session: async (session, user) => {
                console.log('data passed to object when signed in', user)
                //  user object there doesn't have all data passed before
                return Promise.resolve(session)
            }
        },
        debug: false
    })

Console logged objects:

received account data from firestore 
{
  phone: '123123123',
  id: 'w2zh88BZzSv5BJeXZeZX',
  email: 'jan@gmail.com',
  name: 'Jan',
  surname: 'Kowalski'
}
data passed to object when signed in 
{
  name: 'Jan',
  email: 'jan@gmail.com',
  iat: 1603900133,
  exp: 1606492133
}

The best thing is, the object (above) always has the same properties. I can pass any object in authorize callback, but in session, user object always has "name, email, iat, exp" ALWAYS. The only thing that changes are values of these two properties in object (name, email). (rest properties - "phone, id, surname" - are missing).

Below there is console logged session object in any react component:

import {
    signIn, 
    signOut,
    useSession
} from 'next-auth/client'

const [ session, loading ] = useSession();
console.log(session)

Photo of console logged session object

What can I do? Do I have to receive data from firestore separately in session callback? Is the server-side rendering of Next.js causing the problem?

MateuszWawrzynski
  • 1,231
  • 2
  • 7
  • 15
  • 1
    Had the same issue, some additional comments on this problem here: https://github.com/nextauthjs/next-auth/issues/312#issuecomment-718786847 – DeBraid Nov 05 '20 at 19:32

2 Answers2

80

I resolved that problem by myself.
This issue thread helped me a lot!
https://github.com/nextauthjs/next-auth/issues/764

Below is the explanation:

callbacks: {
    jwt: async (token, user, account, profile, isNewUser) => {
        //  "user" parameter is the object received from "authorize"
        //  "token" is being send below to "session" callback...
        //  ...so we set "user" param of "token" to object from "authorize"...
        //  ...and return it...
        user && (token.user = user);
        return Promise.resolve(token)   // ...here
    },
    session: async (session, user, sessionToken) => {
        //  "session" is current session object
        //  below we set "user" param of "session" to value received from "jwt" callback
        session.user = user.user;
        return Promise.resolve(session)
    }
}

EDIT: Due to NextAuth update to v4

Version 4 of NextAuth brings some changes to callbacks shown above. Now there is only one argument assigned to jwt and session functions. However, you can destructure it to separate variables. Rest of the code stays the same as before.

https://next-auth.js.org/configuration/callbacks#jwt-callback
https://next-auth.js.org/configuration/callbacks#session-callback

// api/auth/[...nextauth].js

...
callbacks: {
    jwt: async ({ token, user }) => {
        user && (token.user = user)
        return token
    },
    session: async ({ session, token }) => {
        session.user = token.user
        return session
    }
}
...
MateuszWawrzynski
  • 1,231
  • 2
  • 7
  • 15
  • 3
    Thanks, this was a good lead. These days the signature looks more like, `session: async (session, user) => {`. Also, since you're using `async` you can just return `session`, you don't need to use `Promise.resolve`. – ABCD.ca Dec 02 '20 at 04:16
  • 1
    this solved my issue too, thanks for the edit for v4. – Richard Vartan Melkonian Jan 05 '22 at 14:04
  • The property to be attached to token and session objects has to be "user" only. I wasted a lot of time trying to name it something else. Finally made it works with this answer. Thanks! – Chayanin May 23 '22 at 06:25
  • Using federated identity (cognito -> okta), the attribute data I needed was in the `profile` property of the `jwt` function argument i.e. `jwt: async({ token, profile, user }) => { ... }` – Jason Burbage Aug 30 '22 at 02:30
  • I've been struggling with this for username. Thanks so much for sharing your solution! – Stephen Scott Moore Mar 29 '23 at 21:04
  • How did you persist the user field on the session? If the session callback runs again, the new field is not included. – Dr J Jun 28 '23 at 18:29
  • Thank you! I was struggling with this for while. They should probably add this to the Docs – ThePaulin Jul 17 '23 at 03:23
0

I had this problem too, and it turned out to be the functions in the Adapter that caused the problem. In my case, I wanted to use v4 of next-auth, and I wanted to use DynamoDB. As there is no official v4 adapter for Dynamo, I had to write my own, basing it on the publicly available v3 adapter.

One of the functions you have to provide when creating your own adapter is:

async updateUser(user) {
  // use ddb client to update the user record
  // client returns `data`
  return { ...user, ...data.Attributes }
}

It turns out that the data in data.Attributes is what gets passed to your jwt() callback as user. This seems to be different to the v3 implementation.

Therefore in my case I had to structure the dynamodb adapter's updateUser() function to instruct the client to return ALL_NEW and not merely UPDATED_NEW (which is what the v3 adapter does).

My complete updateUser() function is as follows - bear in mind that at this point, I used the recommended dynamodb table structure (which I don't necessarily agree with, especially in my use case, but that's another story)

async updateUser(user) {
    const now = new Date()
    const data = await client.update({
        TableName: tableName,
        Key: {
            pk: `USER#${user.id}`,
            sk: `USER#${user.id}`,
        },
        UpdateExpression:
            "set #emailVerified = :emailVerified, #updatedAt = :updatedAt",
        ExpressionAttributeNames: {
            "#emailVerified": "emailVerified",
            "#updatedAt": "updatedAt",
        },
        ExpressionAttributeValues: {
            ":emailVerified": user.emailVerified?.toISOString() ?? null,
            ":updatedAt": now.toISOString(),
        },
        ReturnValues: "ALL_NEW",
    }).promise()
    
    return { ...user, ...data.Attributes }
},

As a result, I see user fully populated in the jwt() callback:

callbacks: {
    async jwt({ token, user, account, profile, isNewUser }) {
        console.debug("callback jwt user:", user)
        user && (token.user = user)
        return token
    },

The debug output is the complete record for the user from DynamoDB, and can be used as described in @MateuszWawrzynski's answer.

Coder
  • 2,833
  • 2
  • 22
  • 24