fix oidc callback behind reverse proxy

This commit is contained in:
2026-06-16 10:01:13 +08:00
parent 9024f80a70
commit 626b8928f4
5 changed files with 78 additions and 3 deletions

View File

@@ -48,6 +48,7 @@ OIDC_ISSUER=https://casdoor.example.com
OIDC_CLIENT_ID=analytics OIDC_CLIENT_ID=analytics
OIDC_CLIENT_SECRET=replace-me OIDC_CLIENT_SECRET=replace-me
AUTH_SECRET=replace-with-random-secret AUTH_SECRET=replace-with-random-secret
NEXTAUTH_URL=https://your-analytics-domain
# Optional login button label: # Optional login button label:
OIDC_PROVIDER_NAME=Sinodoor OIDC_PROVIDER_NAME=Sinodoor
``` ```
@@ -64,6 +65,8 @@ When OIDC is enabled, configure the provider redirect URI as:
https://your-analytics-domain/api/auth/callback/oidc https://your-analytics-domain/api/auth/callback/oidc
``` ```
`NEXTAUTH_URL` must be the same public `https://` origin that users open through the reverse proxy. This keeps login redirects and callback URLs from using the container listener such as `http://0.0.0.0:8019`.
Partial OIDC configuration is treated as an error instead of falling back to open access. Partial OIDC configuration is treated as an error instead of falling back to open access.
## Deployment ## Deployment

View File

@@ -7,6 +7,13 @@ services:
- "8019:8019" - "8019:8019"
environment: environment:
- SERVICE_URL_ANALYTICS_8019 - SERVICE_URL_ANALYTICS_8019
- PG_CONNECTION_STRING
- OIDC_ISSUER
- OIDC_CLIENT_ID
- OIDC_CLIENT_SECRET
- AUTH_SECRET
- NEXTAUTH_URL
- OIDC_PROVIDER_NAME
networks: networks:
- coolify - coolify

View File

@@ -4,6 +4,7 @@ export interface AuthEnv {
OIDC_CLIENT_SECRET?: string; OIDC_CLIENT_SECRET?: string;
OIDC_PROVIDER_NAME?: string; OIDC_PROVIDER_NAME?: string;
AUTH_SECRET?: string; AUTH_SECRET?: string;
NEXTAUTH_URL?: string;
} }
export interface AuthMode { export interface AuthMode {

26
proxy.test.ts Normal file
View File

@@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test";
import { NextRequest } from "next/server";
import { buildPublicRequestUrl } from "./proxy";
describe("public request URL", () => {
test("uses NEXTAUTH_URL for OIDC callback URLs behind Docker reverse proxies", () => {
const request = new NextRequest("http://0.0.0.0:8019/logs?range=7d");
expect(
buildPublicRequestUrl(request, {
NEXTAUTH_URL: "https://analytics.example.com",
}).href
).toBe("https://analytics.example.com/logs?range=7d");
});
test("falls back to forwarded proxy headers when no public URL is configured", () => {
const request = new NextRequest("http://0.0.0.0:8019/", {
headers: {
"x-forwarded-host": "analytics.example.com",
"x-forwarded-proto": "https",
},
});
expect(buildPublicRequestUrl(request).href).toBe("https://analytics.example.com/");
});
});

View File

@@ -1,6 +1,12 @@
import { getToken } from "next-auth/jwt"; import { getToken } from "next-auth/jwt";
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { getAuthMode, getRequiredAuthSecret, isAuthRoute, isProtectedPath } from "@/lib/auth-config"; import {
getAuthMode,
getRequiredAuthSecret,
isAuthRoute,
isProtectedPath,
type AuthEnv,
} from "@/lib/auth-config";
export async function proxy(request: NextRequest) { export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl; const { pathname } = request.nextUrl;
@@ -24,8 +30,9 @@ export async function proxy(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
} }
const signInUrl = new URL("/api/auth/signin/oidc", request.url); const callbackUrl = buildPublicRequestUrl(request);
signInUrl.searchParams.set("callbackUrl", request.nextUrl.href); const signInUrl = new URL("/api/auth/signin/oidc", callbackUrl);
signInUrl.searchParams.set("callbackUrl", callbackUrl.href);
return NextResponse.redirect(signInUrl); return NextResponse.redirect(signInUrl);
} }
@@ -46,6 +53,37 @@ function authConfigErrorResponse(request: NextRequest, error: string) {
}); });
} }
export function buildPublicRequestUrl(request: NextRequest, env: AuthEnv = process.env): URL {
const origin = getPublicOrigin(request, env);
return new URL(`${request.nextUrl.pathname}${request.nextUrl.search}`, origin);
}
function getPublicOrigin(request: NextRequest, env: AuthEnv): string {
const configuredOrigin = getConfiguredOrigin(env.NEXTAUTH_URL);
if (configuredOrigin) return configuredOrigin;
const forwardedHost = firstHeaderValue(request.headers.get("x-forwarded-host"));
const host = forwardedHost || firstHeaderValue(request.headers.get("host")) || request.nextUrl.host;
const forwardedProto = firstHeaderValue(request.headers.get("x-forwarded-proto"));
const proto = forwardedProto || request.nextUrl.protocol.replace(":", "") || "http";
return `${proto}://${host}`;
}
function getConfiguredOrigin(value: string | undefined): string | null {
if (!value?.trim()) return null;
try {
return new URL(value).origin;
} catch {
return null;
}
}
function firstHeaderValue(value: string | null): string {
return value?.split(",")[0]?.trim() ?? "";
}
function authErrorHtml(error: string): string { function authErrorHtml(error: string): string {
return `<!doctype html> return `<!doctype html>
<html lang="zh"> <html lang="zh">