Next.js Discord

Discord Forum

Handling Access Token Expiration in Next.js Middleware

Answered
M7ilan posted this in #help-forum
Open in Discord
Avatar
# Problem

Hello, I'm currently utilizing middleware to verify whether the user's access token stored in cookies has expired. If it has, the middleware automatically initiates a request to an API I created. This API generates a new access token using the refresh token, also found in the cookies.

However, I'm encountering an issue. When I intentionally expire the token for testing purposes and refresh the page to activate the middleware, I notice that the tokens in the cookies are simply replaced with their previous values, instead of being updated.

The primary objective is to use the middleware to monitor the expiration status of the user's access token. This approach is ideal as the middleware can be activated across the entire application. If the token is found to be expired, the middleware should then acquire new tokens and update them in the cookies.

# Files

## middleware.ts
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export async function middleware(request: NextRequest) {
    const { cookies } = request;
    const logged_in = cookies.get("logged_in");
    const path = request.nextUrl.pathname;

    const access_token = cookies.get("access_token");
    const refresh_token = cookies.get("refresh_token");
    const generated_at = cookies.get("generated_at");
    const expires_in = cookies.get("expires_in");

    // Check if the access token is expired
    if (access_token && refresh_token && generated_at && expires_in) {
        const now = Date.now();
        const expires_at =
            parseInt(generated_at.value) + parseInt(expires_in.value) * 1000;
        if (now > expires_at) {
            // EXPIRED
            console.log("EXPIRED");
            const result = await fetch("https://localhost:3000/api/refresh-token", {
                method: "POST",
                body: JSON.stringify({ refresh_token: refresh_token.value }),
            });

            if (result) {
                console.log("REFRESHED");
            }
        }
    }

    if (path === "/") {
        if (!logged_in) {
            return NextResponse.redirect(new URL("/login", request.url));
        }
    }

    if (path === "/login") {
        if (logged_in) {
            return NextResponse.redirect(new URL("/", request.url));
        }
    }
}


## app\api\refresh-token\route.ts
import { BungieTokensResponseType } from "@/types";
import StoreTokensInCookies from "@/utils/StoreTokensInCookies";

export async function POST(
    request: Request,
): Promise<BungieTokensResponseType | Response> {
    const { refresh_token }: { refresh_token: string } = await request.json();

    if (!refresh_token) {
        return Response.json(
            { message: "No refresh token provided" },
            { status: 400 },
        );
    }

    if (refresh_token) {
        const options = {
            method: "POST",
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
                grant_type: "refresh_token",
                refresh_token,
                client_id: process.env.BUNGIE_CLIENT_ID as string,
                client_secret: process.env.BUNGIE_CLIENT_SECRET as string,
            }),
        };

        const result = await fetch(
            "https://www.bungie.net/Platform/App/OAuth/token/",
            options,
        );
        const Tokens: BungieTokensResponseType = await result.json();
        StoreTokensInCookies(Tokens);
        return new Response(JSON.stringify(Tokens), { status: 200 });
    }

    return Response.json({ message: "Unknown error" }, { status: 500 });
}
Answered by M7ilan
# Solution

The issue was resolved unexpectedly, and I'm not sure exactly how, but it seems to be related to the use of const response = NextResponse.next();.

# Working Files

## middleware.ts
import type { NextRequest } from "next/server";
import type { BungieTokensResponseType } from "./types";
import { NextResponse } from "next/server";
import isTokenExpired from "./utils/isTokenExpired";
import setTokensInCookies from "./utils/setTokensInCookies";
import refreshToken from "./utils/refreshToken";

export async function middleware(request: NextRequest) {
    const { cookies } = request;
    const loggedIn = cookies.has("logged_in");
    const path = request.nextUrl.pathname;

    const accessToken = cookies.get("access_token")?.value;
    const refreshTokenValue = cookies.get("refresh_token")?.value;
    const generatedAt = cookies.get("generated_at")?.value;
    const expiresIn = cookies.get("expires_in")?.value;
    const refreshExpiresIn = cookies.get("refresh_expires_in")?.value;

    if (accessToken && refreshTokenValue && generatedAt && expiresIn && refreshExpiresIn) {
        if (isTokenExpired(generatedAt, refreshExpiresIn)) {
            return NextResponse.redirect(new URL("/login", request.url));
        }

        if (isTokenExpired(generatedAt, expiresIn)) {
            const newTokens: BungieTokensResponseType = await refreshToken(refreshTokenValue);

            if (newTokens && "access_token" in newTokens) {
                const response = NextResponse.next();
                setTokensInCookies(response, newTokens);
                return response;
            }
        }
    }

    if (loggedIn) {
        if (path === "/login") {
            return NextResponse.redirect(new URL("/", request.url));
        }
    }

    if (!loggedIn) {
        if (path === "/") {
            return NextResponse.redirect(new URL("/login", request.url));
        }
    }
}


## utils\refreshToken.ts
export default async function refreshToken(refreshToken: string) {
    try {
        const result = await fetch("https://localhost:3000/api/refresh-token", {
            method: "POST",
            body: JSON.stringify({ refresh_token: refreshToken }),
        });

        if (!result.ok) {
            throw new Error("Failed to refresh token");
        }

        return await result.json();
    } catch (error) {
        console.error("Error refreshing tokens", error);
        return null;
    }
}


## utils\setTokensInCookies.ts
import type { BungieTokensSuccessType } from "@/types";
import { NextResponse } from "next/server";

export default function setTokensInCookies(response: NextResponse, newTokens: BungieTokensSuccessType) {
    response.cookies.set("access_token", newTokens.access_token);
    response.cookies.set("refresh_token", newTokens.refresh_token);
    response.cookies.set("generated_at", Date.now().toString());
    response.cookies.set("expires_in", newTokens.expires_in.toString());
    response.cookies.set("refresh_expires_in", newTokens.refresh_expires_in.toString());
    response.cookies.set("membership_id", newTokens.membership_id);
    response.cookies.set("token_type", newTokens.token_type);
    response.cookies.set("logged_in", "");
}
View full answer

1 Reply

Avatar
# Solution

The issue was resolved unexpectedly, and I'm not sure exactly how, but it seems to be related to the use of const response = NextResponse.next();.

# Working Files

## middleware.ts
import type { NextRequest } from "next/server";
import type { BungieTokensResponseType } from "./types";
import { NextResponse } from "next/server";
import isTokenExpired from "./utils/isTokenExpired";
import setTokensInCookies from "./utils/setTokensInCookies";
import refreshToken from "./utils/refreshToken";

export async function middleware(request: NextRequest) {
    const { cookies } = request;
    const loggedIn = cookies.has("logged_in");
    const path = request.nextUrl.pathname;

    const accessToken = cookies.get("access_token")?.value;
    const refreshTokenValue = cookies.get("refresh_token")?.value;
    const generatedAt = cookies.get("generated_at")?.value;
    const expiresIn = cookies.get("expires_in")?.value;
    const refreshExpiresIn = cookies.get("refresh_expires_in")?.value;

    if (accessToken && refreshTokenValue && generatedAt && expiresIn && refreshExpiresIn) {
        if (isTokenExpired(generatedAt, refreshExpiresIn)) {
            return NextResponse.redirect(new URL("/login", request.url));
        }

        if (isTokenExpired(generatedAt, expiresIn)) {
            const newTokens: BungieTokensResponseType = await refreshToken(refreshTokenValue);

            if (newTokens && "access_token" in newTokens) {
                const response = NextResponse.next();
                setTokensInCookies(response, newTokens);
                return response;
            }
        }
    }

    if (loggedIn) {
        if (path === "/login") {
            return NextResponse.redirect(new URL("/", request.url));
        }
    }

    if (!loggedIn) {
        if (path === "/") {
            return NextResponse.redirect(new URL("/login", request.url));
        }
    }
}


## utils\refreshToken.ts
export default async function refreshToken(refreshToken: string) {
    try {
        const result = await fetch("https://localhost:3000/api/refresh-token", {
            method: "POST",
            body: JSON.stringify({ refresh_token: refreshToken }),
        });

        if (!result.ok) {
            throw new Error("Failed to refresh token");
        }

        return await result.json();
    } catch (error) {
        console.error("Error refreshing tokens", error);
        return null;
    }
}


## utils\setTokensInCookies.ts
import type { BungieTokensSuccessType } from "@/types";
import { NextResponse } from "next/server";

export default function setTokensInCookies(response: NextResponse, newTokens: BungieTokensSuccessType) {
    response.cookies.set("access_token", newTokens.access_token);
    response.cookies.set("refresh_token", newTokens.refresh_token);
    response.cookies.set("generated_at", Date.now().toString());
    response.cookies.set("expires_in", newTokens.expires_in.toString());
    response.cookies.set("refresh_expires_in", newTokens.refresh_expires_in.toString());
    response.cookies.set("membership_id", newTokens.membership_id);
    response.cookies.set("token_type", newTokens.token_type);
    response.cookies.set("logged_in", "");
}
Answer