Búsqueda de sitios web

typescript - Extraño problema con NextJS y NextAuth


Tengo la siguiente configuración de autenticación para NextJS usando un proveedor/servicio de tokens personalizado. Estoy siguiendo esta guía y mi código se ve así

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);

Las clases relevantes son

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
        );
    }
}

y

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

Pero en la devolución de llamada, estoy recibiendo el siguiente error

error - TypeError: El Conjunto de Reclamaciones JWT DEBE ser un objeto en new ProduceJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\produce.js:10:19) en new EncryptJWT (C:\VRPM\Repos\portal\node_modules\jose\dist\node\cjs\jwt\encrypt.js:7:1) en Object.encode (C:\VRPM\Repos\portal\node_modules\next-auth\jwt\index.js:49:16) en async Object.callback (C:\VRPM\Repos\portal\node_modules\next-auth\coreoutes\callback.js:429:22) en async NextAuthHandler (C:\VRPM\Repos\portal\node_modules\next-auth\core\index.js:295:28) en async NextAuthNextHandler (C:\VRPM\Repos\portal\node_modules\next-auth\next\index. js:23:19) en async C:\VRPM\Repos\portal\node_modules\next-auth\next\index.js:59:32 en async Object.apiResolver (C:\VRPM\Repos\portal\node_modules\next\dist\server\api-utils\node.js:184:9) en async DevServer.runApi (C:\VRPM\Repos\portal\node_modules\next\dist\server\next-server.js:403:9) en async Object.fn (C:\VRPM\Repos\portal\node_modules\next\dist\server\base-server.js:493:37) { página: '/api/auth/[...nextauth]'

y no tengo idea de cómo resolverlo ya que los datos que estoy devolviendo son un objeto:

 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
  }
  

¿Alguien puede decirme dónde me estoy equivocando aquí?


He descubierto que si regreso

return { 
    token: token
};

el error desaparece, pero ¿cuál es la manera correcta de hacer esto?

Respuesta #1

Una cosa que noté en todos los ejemplos de implementaciones de next-auth es que la función authorize() está destinada a devolver los detalles del usuario (id, correo electrónico, etc.), no un objeto que contenga un token y los detalles del usuario.

Parece que estás dependiendo incorrectamente del token de este objeto de usuario bajo la función de devolución de llamada jwt() cuando debería derivarse de la propia devolución de llamada.

He configurado un Next.js + next-auth sandbox y ahora estoy revisando tu código.

En lugar de esto:

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;
},

Haz esto:

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

  // at login, save user token to jwt token
  if (user) {
    token.accessToken = user.token.accessToken;
    token.accessTokenExpiry = user.token.accessTokenExpiry;
    token.refreshToken = user.token.refreshToken;
  }
  
  // check if we should refresh the token
  const shouldRefreshTime = user.token.expiry_date_time < clock.nowUtc();
  if (shouldRefreshTime) {
    token = refreshAccessToken(token);
  }

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

También dentro de authorize():

Cambiar esto de:

return session;

Para:

return session.user;

Cambio importante en el jwt() callback:

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

Para:

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

Además, tu callback session() no debería estar devolviendo el refreshToken. Mencionan esta preocupación de seguridad en su documentación.

La devolución de llamada session() debería actualizarse.

De esto:

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

Hacia esto:

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

   // Send properties to the client, like an access_token and user from a provider.
   session.accessToken = token.accessToken;
   session.user = user;

   return session
}