I have the following auth setup for NextJS using a custom token provider/service. I am following this guide and my code looks like
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 relevent 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
) { }
}
But in the callback, I am getting 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]'
and I have no idea how to resolve as the data I am returning is an object:
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
}
Can anyone tell me where I am going wrong here?
I have found that if I return
return {
token: token
};
the error goes away, but what is the right way to do this?
One thing I noticed in all examples of next-auth implementations is that the authorize()
function is meant to return the user's details (id, email, etc), not an object containing a token
& user
details.
It seems like you are incorrectly relying on the token from this user object under the jwt()
callback when it should be derived from the callback itself.
I've setup a Next.js
+ next-auth
sandbox and I'm stepping through your code now.
Instead of this:
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;
},
Do this:
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);
},
Also inside authorize()
:
Change this from:
return session;
To:
return session.user
;
Important change in the jwt()
callback:
jwt: async ({ user }: any) => {
To:
jwt: async ({ token, user }: any) => {
Additionally, your session()
callback should not be returning the refreshToken
. They mention this security concern in their docs.
The session()
callback should be updated.
From this:
session: async ({ session, user, token }) => {
//session.user = user;
//session.token = token;
return session; <--- entire token (refreshToken, etc) being returned...
}
To this:
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
}