typescript - Konstigt problem med NextJS och NextAuth


Jag har följande autentiseringsinställning för NextJS med en egen tokenleverantör/tjänst. Jag följer denna guide och min kod ser ut så här

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

De relevanta klasserna är

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

och

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

Men i återuppringningen får jag följande fel

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

och jag har ingen aning om hur man löser det eftersom datan jag returnerar är ett objekt:

 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
  }
  

Kan någon säga var jag gör fel här?


Jag har upptäckt att om jag återvänder

return { 
    token: token
};

felet försvinner, men vad är det rätta sättet att göra detta?

Svar #1

En sak jag märkte i alla exempel på next-auth-implementationer är att authorize()-funktionen är tänkt att returnera användarens detaljer (id, e-post osv.), inte ett objekt som innehåller token och user-detaljer.

Det verkar som att du felaktigt förlitar dig på token från detta användarobjekt under jwt() callback när det borde hämtas från callbacken själv.

Jag har satt upp en Next.js + next-auth sandlåda och jag går igenom din kod nu.

Istället för detta:

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

Gör detta:

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

Även inne i authorize():

Ändra detta från:

return session;

Till:

return session.user;

Viktig förändring i jwt()-callbacken:

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

Till:

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

Dessutom bör din session()-callback inte returnera refreshToken. De nämner denna säkerhetsrisk i sin dokumentation.

Callbacken session() bör uppdateras.

Från detta:

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

Till detta:

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
}