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

When running `ng serve` or `ng build --prod`, the dist folder is not created in an Angular 4 application

I recently completed building an Angular 4 app using angular-cli version 1.0.4 and generated the production build with the command ng build --prod. However, I encountered a problem as the expected dist folder was not created after executing this command. ...

Http' does not have the 'update' property

I recently implemented Angular 2 Release and utilized 'Http' from '@angular/http' for my project. However, I encountered an error when I invoked the method 'update', which resulted in the following message: "Evidently, th ...

How can I create an Array of objects that implement an interface? Here's an example: myData: Array<myInterface> = []; However, I encountered an issue where the error message "Property 'xxxxxx' does not exist on type 'myInterface[]'" appears

Currently, I am in the process of defining an interface for an array of objects. My goal is to set the initial value within the component as an empty array. Within a file, I have created the following interface: export interface myInterface{ "pictur ...

How can I style <p> tags with a specific font in Tailwind Typography?

For instance, suppose I wish to utilize the markdown as shown below: # AAAAAAAAAa BBBBBBB This would then be interpreted in such a way that AAAAAAAAAa is designated as h1, BBBBBBB as <p>, and the entire content enclosed within a prose div utilizing ...

What is the best way to remove an exported JavaScript file from Node.js?

In my Node.js library package called "OasisLib," there is a file named TypeGenerator.ts. The specific logic within the file is not crucial, but it requires access to files in the filesystem during the project build process. To achieve this, we utilized let ...

Ways to transmit information or notifications from a service to a component

Currently, I am utilizing Angular 6 and have the upload file control on three different screens (three various components). Each of these screens calls the same method UploadFile(). The main issue arises when I need to make any changes to this method, as ...

Issue when utilizing TypeScript MongoDB Definitions (Unable to locate namespace)

I am currently working on implementing MongoDB typings that I installed using the following command: npm install @types/mongodb -D Now, I want to utilize these types within a function like this: export default async function insertOne(collection:any, da ...

Capturing user audio on the client side with Angular

Is there a built-in feature in Angular to record client-side audio input? I have attempted using the p5 library, but it is encountering integration problems. ...

Avoid triggering onClick events on all rows when clicking, aim for only one row to be affected per click

I have a unique situation where I have a table containing rows with a button in each row. When this button is clicked, it triggers an onClick event that adds two additional buttons below the clicked button. The Issue: When I click on a button, the onClick ...

Autocomplete feature in Angular not showing search results

I am currently using ng-prime's <p-autocomplete> to display values by searching in the back-end. Below is the HTML code I have implemented: <p-autoComplete [(ngModel)]="agent" [suggestions]="filteredAgents" name="agents" (completeMethod)="f ...

Implementing custom color names in Material UI using TypeScript

Currently, I am utilizing Material UI alongside TypeScript and attempting to incorporate custom colors into my theme. While everything seems to be functioning properly, the VSCode linter is displaying the following message: Type '{ tan: string; lightR ...

Mastering the proper method for loading an environment variable in NextJS

After transitioning from React to NextJs, I encountered a peculiar situation while working with a Demo component on a demo page; import React from 'react'; import { Hello, Header } from './demo.styled'; export const Demo = () => { ...

Tips for activating AG Grid Column Auto Sizing on your website

The Issue I am experiencing difficulty in getting columns to expand to the size of their content upon grid rendering. Despite following the guidance provided in the official documentation example, and consulting sources such as Stack Overflow, I have att ...

Building upon a React component with TypeScript, we are extending its functionality using a generic type and then leveraging this same generic type

In my component, I have a setup where it takes two props - node and patchCurrentNode. import { css } from '@emotion/react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import React, { PropsWithChildren, useStat ...

What's the best approach for revalidating data with mutate in SWR?

Whenever a new album is created in my app, the post request response includes an updated list of all albums. To enhance the user experience, I wanted the newly created content to automatically show up without requiring a page refresh. Although I am famil ...

Employ a type as a function in Typescript programming

Looking for a way to convert an ID into an object using a specific type. The type accepts personId as a string parameter and returns either a Person or undefined. export type FindPerson = (personId: string) => Person | undefined; I have multiple person ...

Using ternary operator to set multiple variables in setState

Conditional Operator for Setting State in React I am wondering if there is a way to set the state with a variable that holds the correct state value using setState method. interface state { isfiltered: array<boolean> } this.setState({ ...

Creating mandatory reactive form fields in Angular 11's HTML code based on conditions

I am facing an issue with two select/dropdown fields in my form. The second dropdown field should render based on a condition *ngIf="selectedStdntList?.packages". However, the problem is that the submit form function stops working even when the c ...

A guide on incorporating a set of components to be utilized as custom HTML elements in Angular

I am in the process of revamping my application to be more modular on the UI side, with a focus on separating different elements including: App header Left navigation panel Main content on the right side of the nav panel I have successfully figured out ...

Using SVG files in NextJS

I'm encountering an issue while trying to import an SVG file in a NextJS project. The error message I keep getting is: ./assets/aboutimg.svg 1:0 Module parse failed: Unexpected token (1:0) You may need an appropriate loader to handle this file type, ...