fix oidc callback behind reverse proxy
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
26
proxy.test.ts
Normal 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/");
|
||||||
|
});
|
||||||
|
});
|
||||||
44
proxy.ts
44
proxy.ts
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user