mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
fix(auth): build verify-email redirects from the configured public origin
Redirects were built relative to `request.url`, which behind a reverse proxy resolves to the container-internal address. Verification succeeded but the browser was sent to localhost:3000, so users saw a connection error instead of the "email verified" confirmation. Add getPublicOrigin() (NEXTAUTH_URL, then NEXT_PUBLIC_APP_URL, falling back to the request origin for local development) and use it for every verify-email redirect. The legacy GET redirect in the watch session route had the same defect and is fixed alongside it.
This commit is contained in:
@@ -2,11 +2,18 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { consumeVerificationToken } from '@/lib/email-verification';
|
import { consumeVerificationToken } from '@/lib/email-verification';
|
||||||
import { rateLimit } from '@/lib/rate-limit';
|
import { rateLimit } from '@/lib/rate-limit';
|
||||||
import { logError } from '@/lib/logger';
|
import { logError } from '@/lib/logger';
|
||||||
|
import { getPublicOrigin } from '@/lib/request-origin';
|
||||||
|
|
||||||
// A raw 32-byte hex token is exactly 64 characters.
|
// A raw 32-byte hex token is exactly 64 characters.
|
||||||
const TOKEN_REGEX = /^[0-9a-f]{64}$/;
|
const TOKEN_REGEX = /^[0-9a-f]{64}$/;
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
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 {
|
try {
|
||||||
// Rate-limit by IP to prevent token enumeration attacks.
|
// Rate-limit by IP to prevent token enumeration attacks.
|
||||||
const limited = await rateLimit(request, 'verify-email');
|
const limited = await rateLimit(request, 'verify-email');
|
||||||
@@ -15,18 +22,18 @@ export async function GET(request: NextRequest) {
|
|||||||
const token = request.nextUrl.searchParams.get('token');
|
const token = request.nextUrl.searchParams.get('token');
|
||||||
|
|
||||||
if (!token || !TOKEN_REGEX.test(token.trim())) {
|
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());
|
const email = await consumeVerificationToken(token.trim());
|
||||||
|
|
||||||
if (!email) {
|
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) {
|
} catch (err) {
|
||||||
logError('Email verification error:', err);
|
logError('Email verification error:', err);
|
||||||
return NextResponse.redirect(new URL('/login?error=VerificationFailed', request.url));
|
return redirectTo('/login?error=VerificationFailed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createHash } from 'crypto';
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { checkRateLimit, getClientIp, rateLimit, rateLimitHeaders } from '@/lib/rate-limit';
|
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 { MAX_SHARE_PASSWORD_LENGTH, validateShareLinkAccess } from '@/lib/share-links';
|
||||||
import {
|
import {
|
||||||
createPendingShareValue,
|
createPendingShareValue,
|
||||||
@@ -48,7 +48,7 @@ function validateSameOriginRequest(request: NextRequest): NextResponse | null {
|
|||||||
|
|
||||||
export async function GET(request: NextRequest, { params }: RouteParams) {
|
export async function GET(request: NextRequest, { params }: RouteParams) {
|
||||||
const { videoId } = await params;
|
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');
|
const legacyShareToken = request.nextUrl.searchParams.get('shareToken');
|
||||||
|
|
||||||
// Keep GET route for backwards compatibility, but never establish session from GET.
|
// Keep GET route for backwards compatibility, but never establish session from GET.
|
||||||
|
|||||||
@@ -16,6 +16,26 @@ function getConfiguredOrigins(): string[] {
|
|||||||
.filter((value): value is string => value !== null);
|
.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> {
|
export function getAllowedRequestOrigins(request: NextRequest): Set<string> {
|
||||||
const origins = new Set<string>();
|
const origins = new Set<string>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user