diff --git a/src/middleware.ts b/src/middleware.ts index 578b00a..bbee32a 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,6 +1,7 @@ import { NextResponse, NextRequest } from "next/server"; import { jwtDecode } from "jwt-decode"; import { ROLE_AREAS } from "@/constants/ROLE_AREAS"; +import { getAuthCookieDomain } from "@/utils/getAuthCookieDomain/getAuthCookieDomain"; interface JwtPayload { role?: string; @@ -23,9 +24,10 @@ const getHomeByRole = (role: string | null) => const findArea = (pathname: string) => ROLE_AREAS.find((area) => pathname === area.home || pathname.startsWith(`${area.home}/`)); -const clearTokens = (response: NextResponse) => { - response.cookies.set("access_token", "", { path: "/", maxAge: 0 }); - response.cookies.set("refresh_token", "", { path: "/", maxAge: 0 }); +const clearTokens = (request: NextRequest, response: NextResponse) => { + const domain = getAuthCookieDomain(request.headers.get("host")); + response.cookies.set("access_token", "", { path: "/", maxAge: 0, domain }); + response.cookies.set("refresh_token", "", { path: "/", maxAge: 0, domain }); return response; }; @@ -44,16 +46,17 @@ export function middleware(request: NextRequest) { return NextResponse.redirect(loginUrl); }; - // 권한이 없는 계정으로 로그인하면 토큰을 지우고 로그인 화면에 안내를 띄운다. + // 권한이 없는 계정이면 로그인 화면에 안내만 띄운다. + // 토큰은 운영 앱과 공유하므로 여기서 지우면 운영 앱 세션까지 끊긴다. const redirectForbidden = () => { const loginUrl = new URL(LOGIN_PATH, request.url); loginUrl.searchParams.set("reason", "forbidden"); - return clearTokens(NextResponse.redirect(loginUrl)); + return NextResponse.redirect(loginUrl); }; if (pathname.startsWith(LOGIN_PATH)) { const reason = request.nextUrl.searchParams.get("reason"); - if (reason === "session-expired") return clearTokens(NextResponse.next()); + if (reason === "session-expired") return clearTokens(request, NextResponse.next()); if (reason !== "forbidden" && accessToken && hasRefreshToken && home) return redirectTo(home); return NextResponse.next(); } diff --git a/src/utils/getAuthCookieDomain/getAuthCookieDomain.ts b/src/utils/getAuthCookieDomain/getAuthCookieDomain.ts new file mode 100644 index 0000000..4eb24da --- /dev/null +++ b/src/utils/getAuthCookieDomain/getAuthCookieDomain.ts @@ -0,0 +1,25 @@ +const AUTH_COOKIE_PARENT_DOMAIN = "finditem.kr"; + +/** + * 토큰 쿠키를 지울 때 붙일 `domain` 값을 접속 호스트로 판정합니다. + * + * @remarks + * - 백엔드가 finditem.kr 계열 요청에는 토큰 쿠키를 상위 도메인으로 발급하므로, 지울 때도 같은 도메인을 줘야 지워집니다. + * - dev 서버는 middleware의 `nextUrl`을 자기 주소로 만들기 때문에 `Host` 헤더를 넘겨 판정합니다. + * + * @param host - 요청의 `Host` 헤더 값 (포트 포함 가능) + * @returns finditem.kr 또는 그 하위 도메인이면 `".finditem.kr"`, 아니면 `undefined` + * + * @example + * ```ts + * getAuthCookieDomain("a.finditem.kr"); // ".finditem.kr" + * getAuthCookieDomain("localhost:3000"); // undefined + * ``` + */ +export const getAuthCookieDomain = (host: string | null) => { + const hostname = host?.split(":")[0] ?? ""; + return hostname === AUTH_COOKIE_PARENT_DOMAIN || + hostname.endsWith(`.${AUTH_COOKIE_PARENT_DOMAIN}`) + ? `.${AUTH_COOKIE_PARENT_DOMAIN}` + : undefined; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 8b5e27a..2b7f453 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,3 +14,4 @@ export { applyFiltersToUrl } from "./applyFiltersToUrl/applyFiltersToUrl"; export { getCategoryLabel } from "./getCategoryLabel/getCategoryLabel"; export { getServiceUrl } from "./getServiceUrl/getServiceUrl"; export { isValidCallbackUrl } from "./isValidCallbackUrl/isValidCallbackUrl"; +export { getAuthCookieDomain } from "./getAuthCookieDomain/getAuthCookieDomain";