With Refresh Token is an wrapper middleware for Next.js that helps you to use refresh token to get new access token when the access token is expired.
Install the package with your package manager of choice.
npm install with-refresh-token
Then import the function to get the middleware and configure it with your own options.
// src/lib/with-refresh-token.ts
import { getMiddleware } from "with-refresh-token";
export const withRefreshToken = getMiddleware({
/* options */
});
Finally, import withRefreshToken
and use it in your middleware stack.
// src/middleware.ts
import { withRefreshToken } from "./lib/with-refresh-token";
export const config = { /* config */ }; // see at the end the suggested `config`
function middleware(
_req: NextRequest,
res: NextResponse,
_event: NextFetchEvent
) {
// do other stuff here
return res;
}
export default withRefreshToken(middleware);
// or just
// export default withRefreshToken();
The getMiddleware
function takes an object with the following properties:
shouldRefresh: (req: NextRequest) => boolean
- A function that returnstrue
if the access token should be refreshed.fetchTokenPair: (req: NextRequest) => Promise<TokenPair>
- A function that fetches new token pair.onSuccess: (res: NextResponse, tokenPair: TokenPair) => void
- A function that is called when the new token pair is fetched successfully.onError?: (req: NextRequest, res: NextResponse, error: unknown) => NextResponse | void
- An optional function that is called when an error occurs.
TokenPair
is an object with the following properties:accessToken: string
- The new access token.refreshToken: string
- The new refresh token.
import { NextResponse } from "next/server";
import { getMiddleware } from "with-refresh-token";
const DEFAULT_OFFSET_SECONDS = 15; // refresh the token 15 seconds before it expires
export const withRefreshToken = getMiddleware({
// get the jwt access token from the cookies through the original request
// and check if it is expired
shouldRefresh: (req) => {
const accessToken = req.cookies.get("access-token")?.value;
if (!accessToken) return true; // user is not logged in but may have a refresh token
try {
const exp = jwtDecode(accessToken).exp; // decode the jwt and get the expiration time
if (!exp) return false; // token does not have an expiration time
return exp - DEFAULT_OFFSET_SECONDS <= Date.now() / 1000; // check if the token is expired
} catch {
return true; // invalid token but a refresh token may be available
}
},
fetchTokenPair: async (req) => {
// if true is returned from shouldRefresh, this function will be called
// do whatever you need to get the new token pair
const refreshToken = req.cookies.get("refresh-token")?.value;
if (!refreshToken) {
// this error is caught by the onError function or ignored if not provided, following
// the middleware flow normally
throw new Error("Refresh token not found");
}
const response = await fetch("http://localhost:3000/api/refresh-token", {
method: "POST",
body: JSON.stringify({ refreshToken }),
headers: { "Content-Type": "application/json" },
});
return await response.json(); // `TokenPair` object should be returned
},
onSuccess: (res, tokenPair) => {
// with the new token pair, set the new access token and refresh token to the cookies
// so that the middleware and Server Components can use the new token to make auth requests
res.cookies.set({
name: "access-token",
value: tokenPair.accessToken,
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
});
res.cookies.set({
name: "refresh-token",
value: tokenPair.refreshToken,
maxAge: 60 * 60 * 24 * 7, // 7 days
path: "/",
});
},
onError: (_req, _res, error) => {
// optional function that is called when an error occurs during the refresh token process
// you can clear the cookies and redirect to the login page if is unauthorized
// if (error instanceof Response && error.status === 401) {
// res.cookies.set({ name: "access-token", value: "", maxAge: 0, path: "/" });
// res.cookies.set({ name: "refresh-token", value: "", maxAge: 0, path: "/" });
// return NextResponse.redirect(new URL("/login", req.url));
// }
console.error(error);
},
});
This configuration suggestion is available in the Next.js documentation itself. It will prioritize the execution of the middleware on all accessed pages. Adapt it according to your middleware needs.
// ...
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
has: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
has: [{ type: "header", key: "x-present" }],
missing: [{ type: "header", key: "x-missing", value: "prefetch" }],
},
],
};
// ...
The with-refresh-token
middleware is designed to be as flexible as possible. It provides a way to refresh the access token when it is expired, but it does not enforce any specific way to do so. You can use any method to get the new token pair, as long as it returns a TokenPair
object.