I am new to NextJS, it has somewhat been forced on me and the existing code was not authenticating correctly (after a period the site would hang - not return to login - this was due to the way we were using the refresh token). I decided to roll my sleeves up and re-write the way we were trying to do authentication following what was there (what was hanging off the session object and now required all over the code), whilst also re-designing the way refresh was done following this and another few resources. This is what I have comeup with...
_app.tsx
export default function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
const [interval, setInterval] = useState(0);
return (
<SessionProvider session={session} refetchInterval={interval}>
<RouteGuard>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<RefreshTokenHandler setInterval={setInterval} />
</ThemeProvider>
</RouteGuard>
</SessionProvider>
);
}
where RefreshTokenHandler is
const RefreshTokenHandler = (props: any) => {
const { data: session } = useSession();
useEffect(() => {
if (session != null &&
session.token != null) {
const clock = new Clock();
const expiresDateTime = new Date(session.token?.expiresDateTime);
const timeRemainingSeconds = Math.round((expiresDateTime.getTime() - clock.nowUtc().getTime()) / 1000);
props.setInterval(timeRemainingSeconds > 0 ? timeRemainingSeconds : 0);
}
}, [session]);
return null;
}
export default RefreshTokenHandler;
Clock is
export class Clock implements IClock {
public nowSeconds(): number {
return new Date().getTime() / 1000;
}
public now(): Date {
return new Date();
}
public nowUtc(): Date {
var date = new Date();
return new Date(
date.getUTCFullYear(),
date.getUTCMonth(),
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
date.getUTCSeconds(),
date.getUTCMilliseconds()
);
}
public nowISOString(): string {
return this.now().toISOString().split('.')[0] + 'Z';
}
}
Now, the main part is the [...nextauth.js] file
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
username: { label: 'Username', type: 'text' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials, req) {
try {
if (!credentials) {
return null;
}
const { username, password } = credentials;
const token = await AuthApi.login(username, password);
const user: any = {
token: token
};
if (token != null && token.error == '') {
const userDetails = await AuthApi.getUserDetails(token.accessToken);
if (userDetails != null) {
user.userDetails = userDetails;
}
return user;
}
return null;
} 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, token }: any) => {
if (user) {
token.token = user.token;
token.userDetails = user.userDetails;
}
const clock = new Clock();
const shouldRefreshToken = token.expiresDateTime < clock.nowUtc();
if (!shouldRefreshToken) {
return token;
}
token = await refreshAccessToken(token);
return token;
},
session: async ({ session, token, user }: any) => {
session.token = token.token;
session.userDetails = token.userDetails;
return Promise.resolve(session);
}
}
};
export default NextAuth(authOptions);
async function refreshAccessToken(tokenObject: any) {
try {
const tokenResponse = await AuthApi.refreshToken(tokenObject.refreshToken);
return {
...tokenObject,
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
expiresDateTime: tokenResponse.expiresDateTime,
}
} catch (error) {
return {
...tokenObject,
error: "An error occurred whilst refreshing token"
};
}
}
The refreshToken method returns an AuthToken object
public static async refreshToken(refreshToken: string): Promise<AuthToken> {
const body: any = {
address: 'token_endpoint',
grant_type: 'refresh_token',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
scope: 'offline_access'
};
const url = `/authorization/token`;
const authResponse = await AuthAxios.post(url, qs.stringify(body), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(result => JSON.parse(result.data));
return new AuthToken(
authResponse.access_token,
authResponse.token_type,
authResponse.expires_in,
authResponse.refresh_token
);
}
with AuthToken.ts
export default class AuthToken implements IAuthToken {
public expiresDateTime: Date;
constructor(
public accessToken: string,
public tokenType: string,
public expiresIn: number,
public refreshToken: string,
public error: string = ""
) {
const clock = new Clock();
this.expiresDateTime = new Date(
clock.nowUtc().getTime() + this.expiresIn * 1000
);
}
}
All of this seems to work as I want and due to the way I am embelishing the session object in the session callback
session: async ({ session, token, user }: any) => {
session.token = token.token;
session.userDetails = token.userDetails;
return Promise.resolve(session);
}
I get all of the information I need in my pages and components. In the pages and components, I do
import { authOptions } from '../auth/[...nextauth]';
import { getServerSession } from 'next-auth';
...
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const session = await getServerSession(req, res, authOptions);
if (!session) {
// Not signed in.
res.status(401);
return;
}
const userName = session?.userDetails?.userName!;
const organizationId = session.userDetails?.organizationDetails.organizationId;
const token = session?.token?.accessToken as string;
const searchRegex = req.query.hasOwnProperty('search')
? new RegExp(`${(req.query.search as string)!.toLowerCase()}`, 'i')
: null;
// More code here ...
My question:
Is this approach the correct way of doing this, are there any obvious improvements I can make, or glaring mistake I have made?
Thanks very much for your time.