An unusual problem encountered while working with NextJS and NextAuth

In my NextJS authentication setup, I am using a custom token provider/service as outlined in this guide. The code structure is as follows:

async function refreshAccessToken(authToken: AuthToken) {
    try {
        const tokenResponse = await AuthApi.refreshToken(authToken.refresh_token);
        return new AuthToken(
            tokenResponse.access_token,
            tokenResponse.token_type,
            tokenResponse.expires_in,
            tokenResponse.refresh_token
        );

    } catch (error) {
        return new AuthToken(
            authToken?.access_token,
            authToken?.token_type,
            authToken?.expires_in,
            authToken?.refresh_token,
            "An error occurred whilst refreshing token"
        );
    }
}

export const authOptions: NextAuthOptions = {
    providers: [
        CredentialsProvider({
            name: 'credentials',
            credentials: {
                username: { label: 'Username', type: 'text' },
                password: { label: 'Password', type: 'password' }
            },
            authorize: async (credentials) => {
                try {
                    if (!credentials) {
                        return null;
                    }

                    const { username, password } = credentials;
                    const authToken = await AuthApi.login(username, password);

                    const session: Session = new Session(authToken, null);
                    if (authToken != null && authToken.error == '') {
                        const userDetails = await AuthApi.getUserDetails(authToken.access_token);
                        if (userDetails != null) {
                            session.user = userDetails;
                        }
                    }
                    console.log(session);
                    return session as any;

                } catch (error) {
                    console.error(error);
                    throw error;
                }
            }
        })
    ],
    session: {
        strategy: 'jwt',
        maxAge: Constants.AccessTokenLifetimeSeconds
    },
    secret: process.env.APP_SECRET,
    jwt: {
        secret: process.env.APP_SECRET,
        maxAge: Constants.AccessTokenLifetimeSeconds
    },
    pages: { signIn: '/login' },
    callbacks: {
        jwt: async ({ user }: any) => {
    
            let token: AuthToken | null = null;

            if (user != null && user.token != null) {

                token = user.token;

                const clock = new Clock();
                if (user.token.expiry_date_time < clock.nowUtc()) {
                    token = await refreshAccessToken(user.token);
                }
            }
            console.log("OO", user, token);
            return token;
        },
        session: async ({ session, user, token }) => {
            //session.user = user;
            //session.token = token;
            return session;
        }
    }
};
export default NextAuth(authOptions);

The relevant classes are:

export default class AuthToken implements IAuthToken {

    public expiry_date_time: Date;

    constructor(
        public access_token: string,
        public token_type: string,
        public expires_in: number,
        public refresh_token: string,
        public error: string = ""
    ) {
        const clock = new Clock();
        this.expiry_date_time = new Date(
            clock.nowUtc().getTime() + (this.expires_in - 60) * 1000
        );
    }
}

and

export default class Session implements ISession {
    constructor(
        public token: AuthToken | null,
        public user: UserDetails | null
    ) { }
}

However, when handling the callback, I encounter the following error:

error - TypeError: JWT Claims Set MUST be an object at new ProduceJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\produce.js:10:19) at new EncryptJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\encrypt.js:7:1) at Object.encode (C:\VRPM\Repos\portal\node_modules\next-auth\jwt\index.js:49:16) at async Object.callback (C:\VRPM\Repos\portal\node_modules\next-auth\core\routes\callback.js:429:22) at async NextAuthHandler (C:\VRPM\Repos\portal\node_modules\next-auth\core\index.js:295:28) at async NextAuthNextHandler (C:\VRPM\Repos\portal\node_modules\next-auth\next\index.js:23:19) at async C:\VRPM\Repos\portal\node_modules\next-auth\next\index.js:59:32 at async Object.apiResolver (C:\VRPM\Repos\portal\node_modules\next\dist\server\api-utils\node.js:184:9) at async DevServer.runApi (C:\VRPM\Repos\portal\node_modules\next\dist\server\next-server.js:403:9) at async Object.fn (C:\VRPM\Repos\portal\node_modules\next\dist\server\base-server.js:493:37) { page: '/api/auth/[...nextauth]'

I am returning an object, and yet the error persists:

 token: AuthToken {
    access_token: '...Nn_ooqUtFvdfh53k',
    token_type: 'Bearer',
    expires_in: 86400,
    refresh_token: '...ZddCpNTc',
    error: '',
    expiry_date_time: 2023-02-09T19:29:15.307Z
  }
  

If I modify it to

return { 
    token: token
};

the error disappears. Can someone shed light on the correct approach here?

Answer №1

After reviewing several examples of next-auth implementations, a common observation is that the authorize() function is designed to provide the user's details (such as id and email) rather than an object containing both a token and user details.

It appears that there may be a misconception in relying on the token from the user object within the jwt() callback when it should actually be derived directly from the callback itself.

I have created a setup using a Next.js + next-auth sandbox environment to delve into your code.

Instead of the following:

jwt: async ({ user }: any) => {
  let token = user.token;

  if (user != null && user.token != null) {
      const clock = new Clock();
      if (user.token.expiry_date_time < clock.nowUtc()) {
          token = await refreshAccessToken(user.token);
      }
  }
  return token;
},

Consider this alternative approach:

jwt: async ({ token, user }: any) => {

  // Storing the user token as jwt token during login
  if (user) {
    token.accessToken = user.token.accessToken;
    token.accessTokenExpiry = user.token.accessTokenExpiry;
    token.refreshToken = user.token.refreshToken;
  }
  
  // Check if token refresh is needed
  const shouldRefreshTime = user.token.expiry_date_time < clock.nowUtc();
  if (shouldRefreshTime) {
    token = refreshAccessToken(token);
  }

  // Return the updated token
  return Promise.resolve(token);
},

Furthermore, amendments are necessary inside the authorize() function:

Replace:

return session;

With:

return session.user;

A critical change must be made in the jwt() callback:

jwt: async ({ user }: any) => {

Should be changed to:

jwt: async ({ token, user }: any) => {

Additionally, ensure that your session() callback does not expose the refreshToken. Referencing the concern highlighted in their documentation.

The session() callback requires revision:

From:

session: async ({ session, user, token }) => {
   //session.user = user;
   //session.token = token;
   return session; <--- returning entire token including refreshToken...
}

To:

session: async ({ session, user, token }) => {

   // Include properties like access_token and user data from provider to send to client.
   session.accessToken = token.accessToken;
   session.user = user;

   return session
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

I prefer not to run the next.js SWR until after the initial rendering

Development Setup ・ next.js ・ typescript ・ swr This uses swr for communication purposes. I am looking to only trigger it when the query value changes. However, it is also being executed during the initial rendering. How can I prevent it ...

Creating a proxy using the Next.js API is not compatible with the next-http-proxy-middleware package

I'm attempting to create a proxy using the Next.js API, but it appears to be malfunctioning. This is how my pages/api/index.js file looks: import { NextApiRequest, NextApiResponse } from 'next'; import httpProxyMiddleware from 'next-ht ...

What is the best way to convert the NextJS router.query.id to a string?

As a newcomer to TypeScript and the T3 stack (React Query / Tanstack Query), I am facing an issue with typing companyId as string. I want to avoid having to type companyId as string every time I use it in my code, but I'm struggling to find the best p ...

Discover the power of catching Custom DOM Events in Angular

When working with an Angular library, I encountered a situation where a component within the library dispatches CustomEvents using code like the following: const domEvent = new CustomEvent('unselect', { bubbles: true }); this.elementRef.nati ...

"Embracing the power of multiple inheritance with Types

I am struggling with the concept of multiple inheritance in TypeScript. It doesn't make sense to overload a hierarchy with too much functionality. I have a base class and several branches in the hierarchy. However, I need to utilize mixins to isolate ...

Toggling multiple ions simultaneously does not function independently

I encountered a problem while working on an ionic app. I needed to have individual control over toggle switches, but my current code toggles all switches at once whenever one switch is tapped. Is there a way to fix this and manage each toggle switch separa ...

What are the steps to integrate openjphjs with next.js?

I am working on a project with Next.js and need to utilize openjphjs for decoding HTJ2K pixel data. In order to incorporate openjphjs, I have included both openjphjs.js and openjphjs.wasm in a specific folder within the project structure. To address an e ...

Prevent entry to a specific directory within NextJS

Is there a method to restrict access to the directory in NextJS? For instance, if I wish to prevent access to the directory /public/images? In that case, I would serve images through api routes. So, the image would be displayed in a component like this: & ...

Tips for handling Firebase JS SDK errors within try-catch blocks

Attempting to type the err object in a function that saves documents to Firestore has been challenging. async function saveToFirestore(obj: SOME_OBJECT, collection: string, docId: string) { try { await firebase.firestore().collection(collection).doc( ...

Issue with Angular 2 Custom Pipe not Refreshing Unless Object is Saved

I recently created a custom Angular 2 pipe that formats a phone number from a 10-digit string to 'XXX-XXX-XXXX'. The pipe is functioning perfectly, but the issue arises when it fails to update in real-time during keypress; instead, it updates onl ...

What is the best way to filter or choose tuples based on their inclusion in a certain group

I am working with a tuple object that contains nested tuples. const foo = [ { id: 't1', values: ['a', 'b'] }, { id: 't2', values: ['a', 'c'] }, { id: 't3', values: ['b', ...

How can I display input only when a checkbox is selected? React with Next.js

I'm trying to figure out how to handle this task, but I'm a bit confused on the approach. I would like to display the promo code field only when the checkbox (I have a promo code) is checked. Additionally, it would be ideal to reveal this field ...

What is the process for importing a JSON5 file in Typescript, just like you would with a regular JSON file?

I am looking to import a JSON5 file into a JavaScript object similar to how one can import a JSON file using [import config from '../config.json']. When hovering over, this message is displayed but it's clearly visible. Cannot find module & ...

A guide to accessing an ngModel element within a reusable component

I have a specific ngModel component inside a reusable component that is not part of a form. I need to access it in order to make some changes, but when I try to do so using the code below, it returns undefined during OnInit. Can you advise me on how to pro ...

Status:0 was received as the response from URL:null during the REST call made from my iOS Ionic application

I am currently facing an issue with a rest call in my Ionic app. The call works fine on Android devices but encounters problems on iOS devices. Below is the implementation of the rest call in my Ionic service. import { Http } from '@angular/http&apos ...

Issue with displaying Angular index.html page post-build

My Angular application runs smoothly on ng serve, but after building and uploading with ng build --prod, the index.html file fails to open. I've tried using various base href configurations like <base href="#">, <base href="/& ...

Having trouble retrieving the 'WWW-authenticate' header from the $http response in Angular?

Can someone explain why the 'WWW-Authenticate' header appears to be null in the response, even though it is visible as a stringified object in Chrome dev tools? On the server side, I have set the WWW-Authenticate header and configured the necess ...

Encountering the error "Invalid URL. Please provide only absolute URLs" while trying to integrate the Airtable JavaScript library in a Next.js application

I am currently working on a Next JS application that includes a Page called somePage.js. In this page, I am attempting to make an XHR request to the Airtable API from within the getServerSideProps function. The relevant components look like this: pages/so ...

Issue with the maximum cache size in Next.js causing excessive API calls

In my experience with Next.js, I've encountered a frustrating limitation on cache size for fetch requests. The 2MB limit means that some of my fetches exceed this threshold and end up not being cached. This becomes particularly bothersome because ever ...

Is it possible for me to create a union type that connects parameters and responses in a cohesive manner

I'm interested in creating a custom type that functions can use to indicate to callers that an input parameter of a specific type corresponds to a certain output type. For instance, consider the following scenario: type ResponseMap = { requestPath: ...