fix: use forwarded host for auth redirects
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
Some checks failed
CI - Production Readiness / Verify (push) Has been cancelled
This commit is contained in:
@ -4,6 +4,7 @@ import { SESSION_COOKIE, UserRole, authenticateUser, getDefaultPathForRole, seri
|
|||||||
import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit";
|
import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit";
|
||||||
import { consumeRateLimit, getRateLimitHeaders } from "@/lib/rate-limit";
|
import { consumeRateLimit, getRateLimitHeaders } from "@/lib/rate-limit";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { getRequestBaseUrl } from "@/lib/request-url";
|
||||||
|
|
||||||
function getSafePath(value: string | null) {
|
function getSafePath(value: string | null) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -28,6 +29,7 @@ function resolveNumber(raw: string | undefined, fallback: number) {
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const { ipAddress, userAgent } = await getRequestAuditContext();
|
const { ipAddress, userAgent } = await getRequestAuditContext();
|
||||||
|
const baseUrl = getRequestBaseUrl(request);
|
||||||
const retryControl = consumeRateLimit(ipAddress || "unknown", {
|
const retryControl = consumeRateLimit(ipAddress || "unknown", {
|
||||||
scope: "auth_login",
|
scope: "auth_login",
|
||||||
limit: resolveNumber(process.env.LOGIN_RATE_LIMIT_ATTEMPTS, 10),
|
limit: resolveNumber(process.env.LOGIN_RATE_LIMIT_ATTEMPTS, 10),
|
||||||
@ -35,7 +37,7 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!retryControl.allowed) {
|
if (!retryControl.allowed) {
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", baseUrl);
|
||||||
loginUrl.searchParams.set("error", "rate_limited");
|
loginUrl.searchParams.set("error", "rate_limited");
|
||||||
const response = NextResponse.redirect(loginUrl);
|
const response = NextResponse.redirect(loginUrl);
|
||||||
const headers = getRateLimitHeaders(retryControl);
|
const headers = getRateLimitHeaders(retryControl);
|
||||||
@ -55,7 +57,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const password = typeof rawPassword === "string" ? rawPassword : "";
|
const password = typeof rawPassword === "string" ? rawPassword : "";
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", baseUrl);
|
||||||
loginUrl.searchParams.set("error", "credentials_required");
|
loginUrl.searchParams.set("error", "credentials_required");
|
||||||
if (next) {
|
if (next) {
|
||||||
loginUrl.searchParams.set("next", next);
|
loginUrl.searchParams.set("next", next);
|
||||||
@ -88,7 +90,7 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", baseUrl);
|
||||||
loginUrl.searchParams.set("error", "invalid_credentials");
|
loginUrl.searchParams.set("error", "invalid_credentials");
|
||||||
if (next) {
|
if (next) {
|
||||||
loginUrl.searchParams.set("next", next);
|
loginUrl.searchParams.set("next", next);
|
||||||
@ -116,7 +118,7 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const destination = next ?? getDefaultPathForRole(session.role as UserRole);
|
const destination = next ?? getDefaultPathForRole(session.role as UserRole);
|
||||||
const response = NextResponse.redirect(new URL(destination, request.url));
|
const response = NextResponse.redirect(new URL(destination, baseUrl));
|
||||||
response.cookies.set(SESSION_COOKIE, await serializeSession(session), {
|
response.cookies.set(SESSION_COOKIE, await serializeSession(session), {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
|
|
||||||
import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit";
|
import { getRequestAuditContext, writeAuditTrail } from "@/lib/audit";
|
||||||
import { getSession, SESSION_COOKIE } from "@/lib/auth";
|
import { getSession, SESSION_COOKIE } from "@/lib/auth";
|
||||||
|
import { getRequestBaseUrl } from "@/lib/request-url";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
@ -20,7 +21,7 @@ export async function GET(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = NextResponse.redirect(new URL("/login", request.url));
|
const response = NextResponse.redirect(new URL("/login", getRequestBaseUrl(request)));
|
||||||
response.cookies.delete(SESSION_COOKIE);
|
response.cookies.delete(SESSION_COOKIE);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
19
lib/request-url.ts
Normal file
19
lib/request-url.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export function getRequestBaseUrl(request: NextRequest) {
|
||||||
|
const configured = process.env.APP_URL?.trim();
|
||||||
|
if (configured) {
|
||||||
|
return new URL(configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardedHost = request.headers.get("x-forwarded-host");
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto");
|
||||||
|
const host = forwardedHost?.split(",")[0]?.trim() || request.headers.get("host") || request.nextUrl.host;
|
||||||
|
const proto = (forwardedProto?.split(",")[0]?.trim() || request.nextUrl.protocol || "http").replace(":", "");
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return request.nextUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(`${proto}://${host}`);
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import { NextResponse, type NextRequest } from "next/server";
|
|||||||
|
|
||||||
import { canAccessPath, getDefaultPathForRole, parseSessionCookie, SESSION_COOKIE, type UserRole } from "@/lib/auth";
|
import { canAccessPath, getDefaultPathForRole, parseSessionCookie, SESSION_COOKIE, type UserRole } from "@/lib/auth";
|
||||||
import { DEFAULT_LOCALE, isLocale, LOCALE_COOKIE } from "@/lib/i18n";
|
import { DEFAULT_LOCALE, isLocale, LOCALE_COOKIE } from "@/lib/i18n";
|
||||||
|
import { getRequestBaseUrl } from "@/lib/request-url";
|
||||||
|
|
||||||
const publicPaths = ["/login", "/forgot-password", "/reset-password", "/unauthorized", "/invite", "/auth"];
|
const publicPaths = ["/login", "/forgot-password", "/reset-password", "/unauthorized", "/invite", "/auth"];
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ async function decodeSessionCookie(value: string) {
|
|||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
const baseUrl = getRequestBaseUrl(request);
|
||||||
const response = NextResponse.next();
|
const response = NextResponse.next();
|
||||||
|
|
||||||
if (pathname.startsWith("/_next") || pathname.includes(".")) {
|
if (pathname.startsWith("/_next") || pathname.includes(".")) {
|
||||||
@ -44,17 +46,17 @@ export async function middleware(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!session && !isPublicPath(pathname) && pathname !== "/") {
|
if (!session && !isPublicPath(pathname) && pathname !== "/") {
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", baseUrl);
|
||||||
loginUrl.searchParams.set("next", pathname);
|
loginUrl.searchParams.set("next", pathname);
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && (pathname === "/" || pathname === "/login")) {
|
if (session && (pathname === "/" || pathname === "/login")) {
|
||||||
return NextResponse.redirect(new URL(getDefaultPathForRole(session.role), request.url));
|
return NextResponse.redirect(new URL(getDefaultPathForRole(session.role), baseUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session && !isPublicPath(pathname) && !canAccessPath(session.role, pathname)) {
|
if (session && !isPublicPath(pathname) && !canAccessPath(session.role, pathname)) {
|
||||||
return NextResponse.redirect(new URL("/unauthorized", request.url));
|
return NextResponse.redirect(new URL("/unauthorized", baseUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
Reference in New Issue
Block a user