Skip to content

Bug: Token-refresh route redirects to 0.0.0.0:3000 when running behind a reverse proxy #417

@aron-muon

Description

@aron-muon

Description

After upgrading from v0.4.0 to v0.4.1, navigating to /catalog (or any authenticated route) results in a redirect to https://0.0.0.0:3000/catalog instead of the correct external URL. The /login page continues to work fine.

This was introduced by the new preemptive token-refresh mechanism added in v0.4.1.

Environment

  • Kubernetes (EKS) with AWS ALB Ingress Controller
  • ALB terminates TLS and forwards traffic to pod port 3000
  • BETTER_AUTH_URL is correctly set to the external URL (e.g. https://mcp.example.com)
  • v0.4.0 works correctly; v0.4.1 does not

Steps to Reproduce

  1. Deploy toolhive-cloud-ui v0.4.1 behind a reverse proxy / load balancer (e.g. AWS ALB, nginx, Traefik)
  2. Set BETTER_AUTH_URL to the external URL
  3. Navigate to https://<external-domain>/login — works fine
  4. Navigate to https://<external-domain>/catalog — browser is redirected to https://0.0.0.0:3000/catalog

Root Cause

The new token-refresh Route Handler in src/app/api/auth/token-refresh/route.ts constructs redirect URLs using request.url:

const redirectResponse = NextResponse.redirect(
  new URL(safeRedirect, request.url),
);

Similarly, the open-redirect validation also uses request.nextUrl.origin:

if (resolved.origin === request.nextUrl.origin) {
  safeRedirect = resolved.pathname + resolved.search + resolved.hash;
}

In Next.js 16, request.url for Route Handlers is constructed in next-server.ts attachRequestMeta():

const initUrl =
  this.fetchHostname && this.port
    ? `${protocol}://${this.fetchHostname}:${this.port}${req.url}`
    : this.nextConfig.experimental.trustHostHeader
      ? `https://${req.headers.host || 'localhost'}${req.url}`
      : req.url

Because the Dockerfile sets ENV HOSTNAME="0.0.0.0", this.fetchHostname is "0.0.0.0" and this.port is 3000. This means the first branch always wins, producing https://0.0.0.0:3000/... regardless of what the ALB forwards in the Host header.

The x-forwarded-proto IS respected (hence https://), but the host is hardcoded to the server bind address.

Suggested Fixes

There are two ways to fix this. Either (or both) would work:

Option A: Use BETTER_AUTH_URL for redirect construction (application-level fix)

The BETTER_AUTH_URL environment variable already represents the correct external origin and is a required configuration value. The token-refresh Route Handler should use it as the base for redirect URL construction instead of request.url:

import { BASE_URL } from "@/lib/auth/constants";

// Before (broken behind reverse proxy)
const redirectResponse = NextResponse.redirect(
  new URL(safeRedirect, request.url),
);

// After (uses the configured external URL)
const redirectResponse = NextResponse.redirect(
  new URL(safeRedirect, BASE_URL),
);

The same change should apply to the open-redirect origin validation and the /signin fallback redirects in the same file.

Option B: Enable trustHostHeader in next.config.ts (framework-level fix)

Next.js 16 has an experimental config option that tells the server to use the incoming Host header (forwarded by the reverse proxy) instead of the server's bind address when constructing request.url:

// next.config.ts
const nextConfig: NextConfig = {
  experimental: {
    trustHostHeader: true,
  },
  // ...existing config
};

This makes request.url resolve correctly behind any reverse proxy (ALB, nginx, Traefik, etc.) without changing application code. This is the more general fix — any Route Handler using request.url for redirects will benefit.

Attempted Workarounds

__NEXT_PRIVATE_ORIGIN env var — does NOT work. In Next.js 16, this env var is only used in action-handler.ts for Server Actions. It is NOT used in attachRequestMeta() which controls request.url for Route Handlers. Additionally, start-server.ts unconditionally overwrites it at startup.

Pin to v0.4.0 — this is the only working workaround for deployers. The token-refresh flow was introduced in v0.4.1, so v0.4.0 does not have this issue.

Affected Versions

  • Working: v0.4.0
  • Broken: v0.4.1

Related Code

  • src/app/api/auth/token-refresh/route.ts (new in v0.4.1)
  • src/lib/api-client.ts (getAuthenticatedClient — triggers the redirect to token-refresh)
  • src/lib/auth/utils.ts (isTokenNearExpiry — new in v0.4.1)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions