Merge pull request #35 from yusufipk/fix/verify-email-redirect-origin

fix(auth): build verify-email redirects from the configured public or…
This commit is contained in:
Yusuf İpek
2026-07-25 10:59:52 +03:00
committed by GitHub
3 changed files with 33 additions and 6 deletions
+11 -4
View File
@@ -2,11 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
import { consumeVerificationToken } from '@/lib/email-verification';
import { rateLimit } from '@/lib/rate-limit';
import { logError } from '@/lib/logger';
import { getPublicOrigin } from '@/lib/request-origin';
// A raw 32-byte hex token is exactly 64 characters.
const TOKEN_REGEX = /^[0-9a-f]{64}$/;
export async function GET(request: NextRequest) {
// Redirect targets must be built from the public origin, not `request.url`:
// behind a reverse proxy the latter is the container-internal address and the
// user lands on a dead host even though verification succeeded.
const origin = getPublicOrigin(request);
const redirectTo = (path: string) => NextResponse.redirect(new URL(path, origin));
try {
// Rate-limit by IP to prevent token enumeration attacks.
const limited = await rateLimit(request, 'verify-email');
@@ -15,18 +22,18 @@ export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token');
if (!token || !TOKEN_REGEX.test(token.trim())) {
return NextResponse.redirect(new URL('/login?error=InvalidVerificationToken', request.url));
return redirectTo('/login?error=InvalidVerificationToken');
}
const email = await consumeVerificationToken(token.trim());
if (!email) {
return NextResponse.redirect(new URL('/login?error=InvalidVerificationToken', request.url));
return redirectTo('/login?error=InvalidVerificationToken');
}
return NextResponse.redirect(new URL('/login?verified=true', request.url));
return redirectTo('/login?verified=true');
} catch (err) {
logError('Email verification error:', err);
return NextResponse.redirect(new URL('/login?error=VerificationFailed', request.url));
return redirectTo('/login?error=VerificationFailed');
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { createHash } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { checkRateLimit, getClientIp, rateLimit, rateLimitHeaders } from '@/lib/rate-limit';
import { isTrustedSameOriginRequest } from '@/lib/request-origin';
import { getPublicOrigin, isTrustedSameOriginRequest } from '@/lib/request-origin';
import { MAX_SHARE_PASSWORD_LENGTH, validateShareLinkAccess } from '@/lib/share-links';
import {
createPendingShareValue,
@@ -48,7 +48,7 @@ function validateSameOriginRequest(request: NextRequest): NextResponse | null {
export async function GET(request: NextRequest, { params }: RouteParams) {
const { videoId } = await params;
const cleanWatchUrl = new URL(`/watch/${videoId}`, request.nextUrl.origin);
const cleanWatchUrl = new URL(`/watch/${videoId}`, getPublicOrigin(request));
const legacyShareToken = request.nextUrl.searchParams.get('shareToken');
// Keep GET route for backwards compatibility, but never establish session from GET.
+20
View File
@@ -16,6 +16,26 @@ function getConfiguredOrigins(): string[] {
.filter((value): value is string => value !== null);
}
/**
* Origin to build user-facing redirects from.
*
* Behind a reverse proxy (Docker deployments) `request.nextUrl.origin` is the
* container-internal address (`localhost:3000`), so redirecting relative to the
* request URL sends the browser to a dead host. Prefer the operator-configured
* public origin and fall back to the request origin for local development.
*/
export function getPublicOrigin(request: NextRequest): string {
const configured = [process.env.NEXTAUTH_URL, process.env.NEXT_PUBLIC_APP_URL];
for (const value of configured) {
if (typeof value !== 'string' || value.trim().length === 0) continue;
const origin = normalizeOrigin(/^https?:\/\//i.test(value) ? value : `https://${value}`);
if (origin) return origin;
}
return request.nextUrl.origin;
}
export function getAllowedRequestOrigins(request: NextRequest): Set<string> {
const origins = new Set<string>();