Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
};

Expand All @@ -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();
}
Expand Down
25 changes: 25 additions & 0 deletions src/utils/getAuthCookieDomain/getAuthCookieDomain.ts
Original file line number Diff line number Diff line change
@@ -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;
};
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading