문제와 원인
GET 로그아웃과 다른 출처·Origin 없는 POST에서도 세션·OAuth 대기 쿠키 2개를 만료시켰다. GET 라우트가 로그아웃 서비스를 호출했고, 서비스는 메서드와 출처를 검사하기 전에 쿠키 만료 응답을 만들었다.
수정 전 코드
route.ts
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
export const GET = (request: Request) =>
withApiErrorBoundary(request, () => logoutGoogleAuth(request));
logoutGoogleAuth
수정 전 쿠키 만료와 기본 302 리다이렉트:
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
return redirectResponse(new URL(returnTo, origin).toString(), [
clearGoogleSessionCookie(request.url),
clearGooglePendingCookie(request.url),
]);
수정 후 코드
route.ts
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
export const POST = (request: Request) =>
withApiErrorBoundary(request, () => logoutGoogleAuth(request), {
code: "BACKEND_UNAVAILABLE",
message: "서비스를 준비하지 못했습니다. 잠시 후 다시 시도해 주세요.",
});
logoutGoogleAuth
라우트를 POST로 바꾸고 서비스에서 메서드→Origin→쿠키 만료 순서로 처리했다. POST 이외의 요청은 HTTP 405와 Allow: POST를 반환한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
if (request.method !== "POST") {
return new Response(null, {
status: 405,
headers: { Allow: "POST" },
});
}
Origin이 요청 URL의 origin과 다르거나 누락되면 HTTP 403으로 종료한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
const requestOrigin = new URL(request.url).origin;
if (request.headers.get("origin") !== requestOrigin) {
return Response.json(
{ error: "허용되지 않은 요청 출처입니다." },
{ status: 403, headers: noStoreHeaders() },
);
}
두 검사를 통과한 요청에 쿠키 만료 지시 2개와 HTTP 303을 반환한다. 복귀 주소는 기존 safeRelativeReturnPath()로 검증하고, 후속 페이지는 GET으로 읽는다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
return redirectResponse(new URL(returnTo, origin).toString(), [
clearGoogleSessionCookie(request.url),
clearGooglePendingCookie(request.url),
], 303);
검증 결과
Node.js에서 원본 전후 logoutGoogleAuth()에 같은 합성 요청 4개를 넣고 HTTP 상태와 Set-Cookie를 비교했다. Wrangler의 로컬 Worker에도 같은 메서드·Origin 조건으로 HTTP 요청을 보냈다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 입력 조건 | 수정 전 HTTP / 만료 쿠키 | 수정 후 HTTP / 만료 쿠키 |
|---|---|---|
| GET, Origin 없음 | 302 / 2개 | 405 / 0개 |
| POST, 다른 Origin | 302 / 2개 | 403 / 0개 |
| POST, Origin 없음 | 302 / 2개 | 403 / 0개 |
| POST, 동일 Origin | 302 / 2개 | 303 / 2개 |
거절된 세 조건의 Set-Cookie는 비어 있고, GET 거절 응답의 Allow는 POST다. 정상 POST의 복귀 주소는 전후 같은 내부 학습 경로를 유지했다.