I am trying to create a project with nextjs 14 app router. In my project I have used next-auth(5) or auth.js for authentication and authorization. So everything works fine with auth.js but facing a problem with access token expiry time. Though auth.js handle our authentication and authorization for each request .But I want to customize only for credentials. I have set two types (access token and refresh token) of tokens for authentication.In access token I have set access token expiry time 20 seconds. But after successfull login in jwt callbacks access token expiry time set with default expiry time (30 days). So how can I customize the access token expiry value in jwt callbacks? Below I provide the files responsible with authentication and authorization..
auth.js file
import bcrypt from "bcryptjs";
import NextAuth, { AuthError } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import FacebookProvider from "next-auth/providers/facebook";
import GoogleProvider from "next-auth/providers/google";
import { authConfig } from "./auth.config";
import { User } from "./models/user-model";
import { getNewTokens } from "./lib/getNewTokens";
import { refreshAccessToken } from "./lib/refreshAccessToken";
class customError extends AuthError {
constructor(message) {
super();
this.message = message;
}
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
...authConfig,
providers: [
CredentialsProvider({
async authorize(credentials) {
if (!credentials) return null;
try {
// Find the user based on the email provided
const user = await User.findOne({ email: credentials.email }).lean();
if (!user) {
console.error("User not found");
throw new Error("Invalid email or password");
}
// Compare the entered password with the stored password hash
const isPasswordMatch = await bcrypt.compare(
credentials.password,
user?.password
);
if (!isPasswordMatch) {
console.error("Password mismatch");
throw new Error("Invalid email or password");
}
// Return the user object if authentication is successful
return user;
} catch (error) {
// Log and throw the error to be caught in the calling function
throw new customError(error.message);
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
FacebookProvider({
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
}),
],
trustHost:
process.env.NODE_ENV === "production" ? true : ["localhost", "127.0.0.1"],
callbacks: {
async signIn({ user, account, profile }) {
if (account.provider === "google" || account.provider === "facebook") {
try {
const existingUser = await User.findOne({ email: user.email });
if (!existingUser) {
const newUser = new User({
email: user.email,
name: user.name,
password: "sample password",
role: "customer",
});
await newUser.save();
}
return true;
} catch (error) {
console.error("Error saving user to the database:", error);
return false;
}
}
return true;
},
async jwt({ token, user, account }) {
const currentTime = Math.floor(Date.now() / 1000);
const accessTokenExpirationTime = 20;
const refreshTokenExpirationTime = 3600;
// First time logging in (account and user exist)
if (user && account) {
console.log({ user, account, tokenInsideJWT: token });
// For credentials login
if (account.provider === "credentials") {
const newToken = await getNewTokens({
userId: user.id || user._id.toString(),
name: user.firstName + user.lastName,
email: user.email,
});
return {
...token,
accessToken: newToken?.accessToken,
refreshToken: newToken?.refreshToken,
iat: currentTime,
exp: currentTime + accessTokenExpirationTime,
refreshTokenExpires: currentTime + refreshTokenExpirationTime,
provider: account.provider,
user,
};
}
// For social login (Google, Facebook, etc.)
return {
...token,
accessToken: account.access_token,
refreshToken: account.refresh_token,
iat: currentTime,
exp: currentTime + accessTokenExpirationTime,
refreshTokenExpires: currentTime + refreshTokenExpirationTime,
provider: account.provider,
user,
};
}
// Token refresh: Check if the access token has expired
if (Date.now() / 1000 > token.exp) {
const refreshedToken = await refreshAccessToken(token);
console.log(
"Access token has expired, refreshedTokens...",
refreshedToken
);
return {
...refreshedToken,
exp: currentTime + accessTokenExpirationTime,
refreshTokenExpires: currentTime + refreshTokenExpirationTime,
};
}
console.log({ token }); // **Here I got the default value (30 days) of access token expiry time**
// If the token is valid, ensure the custom expiration is maintained
// Here we explicitly ensure that the exp is updated every time, even if the token already exists
if (token && token?.exp) {
return {
...token,
exp: currentTime + accessTokenExpirationTime, // Force the 20-second expiration on each call
};
}
return token;
},
async session({ session, token }) {
console.log({ token }); // **Here I got the customize value (20 sec) of access token expiry time**
const date = new Date(token?.exp * 1000);
const options = {
timeZone: "Asia/Dhaka",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
};
if (token) {
session.user = token?.user;
session.accessToken = token?.accessToken;
session.error = token?.error;
session.expires = new Intl.DateTimeFormat("en-US", options).format(
date
);
}
return session;
},
},
});
if access token expired within 20 seconds I want to regenrate access token as well as refresh token. For this I have created a refreshAccessToken function and this works perfectly…..
export async function refreshAccessToken(token) {
let url = null;
if (token?.provider === "credentials") {
url =
`http://localhost:3000/api/auth/refreshToken?` +
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: token?.refreshToken,
});
}
if (token?.provider === "google") {
url =
`https://oauth2.googleapis.com/token?` +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token?.refreshToken,
});
}
try {
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
});
const refreshedTokens = await response.json();
if (response?.status !== 200 || !response.ok) {
throw refreshedTokens;
}
console.log({ refreshedTokensInsideRefresh: refreshedTokens });
return {
...token,
accessToken:
refreshedTokens?.accessToken || refreshedTokens?.access_token,
refreshToken:
refreshedTokens?.refreshToken || refreshedTokens?.refreshToken,
};
} catch (error) {
console.log(error);
return {
...token,
error: "RefreshAccessTokenError",
};
}
}
this function responsible for creating new tokens….
import { SignJWT } from "jose";
// Function to generate tokens
export const getNewTokens = async (user) => {
const secretKey = new TextEncoder().encode(process.env.AUTH_SECRET);
const currentDate = new Date().toLocaleString("en-US", {
timeZone: "Asia/Dhaka",
});
const timestamp = new Date(currentDate).getTime();
const nowInSeconds = Math.floor(timestamp / 1000);
const payload = {
id: user.userId,
email: user.email,
name: user.name,
};
// Access token (expires in 1 hour)
const accessToken = await new SignJWT({ ...payload, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(nowInSeconds)
.setExpirationTime("20s") // 1 hour from now
.sign(secretKey);
// Refresh token (expires in 1 day)
const refreshToken = await new SignJWT({ ...payload, type: "refresh" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt(nowInSeconds)
.setExpirationTime("1h") // 1 day from now
.sign(secretKey);
return { accessToken, refreshToken };
};
I want to customize the default token of auth.js and set expiry time for each of tokens (access token and refresh token)