Anmeldung: Host-Abweichung frueh erkennen, weniger auf der Anmeldeseite verraten
Im Betrieb aufgetreten: Die Anwendung lief hinter einem Reverse Proxy unter einer Domain, in der .env stand aber noch die interne Adresse aus der Einrichtung. Der Anmeldeknopf setzt den Cookie mit dem laufenden Vorgang auf dem Host, ueber den der Browser da ist — die Rueckkehradresse fuer den Anbieter kommt dagegen aus BASE_URL. Der Anbieter schickte den Browser also auf den anderen Host, der Cookie blieb zurueck, und am Ende stand „Der Anmeldevorgang ist abgelaufen". Eine Meldung, die die Ursache gut versteckt. Jetzt prueft der Start beide Adressen und bricht sofort mit einer Erklaerung ab. Auf dem Bildschirm steht nur die aufgerufene Adresse — die interne gehoert ins Protokoll, nicht auf eine Seite, die jeder sieht. Aus demselben Grund faellt der Hinweis „Die Anmeldung mit Passwort ist abgeschaltet (OIDC_ONLY=true)" weg. Wie die Anmeldung eingerichtet ist, geht Fremde nichts an; wer hierher gehoert, sieht den einen Knopf und weiss, was zu tun ist. Beim allerersten Aufruf bleibt der Hinweis, dass der Erste Administrator wird — der ist dann nuetzlich. Dazu die Anleitungen: Der Reverse-Proxy-Abschnitt im README nennt jetzt die drei Kopfzeilen, die die Anwendung vom Proxy erwartet, und was ohne sie passiert; dazu client_max_body_size fuer Uploads, ein Weg zum Nachpruefen und die Frage, fuer wen die Codes erreichbar sein sollen. In authentik.md steht, warum BASE_URL und die Adresse im Browser dieselbe sein muessen.
This commit is contained in:
@@ -94,12 +94,16 @@ export default async function SignInPage({ searchParams }: PageProps) {
|
||||
Gruppen, Benutzer und die QR-Typen verwalten.
|
||||
</Notice>
|
||||
) : oidc?.nurOidc ? (
|
||||
<p className="text-center text-sm text-slate-500">
|
||||
Die Anmeldung mit Passwort ist abgeschaltet (<code>OIDC_ONLY=true</code>).
|
||||
{setup
|
||||
? " Wer sich zuerst anmeldet, wird Administrator dieser Installation."
|
||||
: ""}
|
||||
</p>
|
||||
/* Bewusst kein Hinweis darauf, dass die Passwort-Anmeldung
|
||||
abgeschaltet ist. Diese Seite sieht jeder, der die Adresse kennt —
|
||||
wie die Anmeldung eingerichtet ist, geht Fremde nichts an. Wer
|
||||
hierher gehört, sieht den einen Knopf und weiß, was zu tun ist.
|
||||
Beim allerersten Aufruf ist der Hinweis dagegen nützlich. */
|
||||
setup ? (
|
||||
<p className="text-center text-sm text-slate-500">
|
||||
Wer sich zuerst anmeldet, wird Administrator dieser Installation.
|
||||
</p>
|
||||
) : null
|
||||
) : (
|
||||
<p className="text-center text-sm text-slate-500">
|
||||
Passwort vergessen? Ein Administrator kann es unter Verwaltung →
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { cookies } from "next/headers";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
import { isAuthEnabled } from "@/lib/auth/actor";
|
||||
import {
|
||||
assertHostMatchesBaseUrl,
|
||||
discover,
|
||||
getOidcConfig,
|
||||
isOidcEnabled,
|
||||
@@ -26,7 +28,7 @@ function base64url(buffer: Buffer): string {
|
||||
* zusätzlich das Geheimnis aus diesem Aufruf nötig ist. Das schützt auch dann,
|
||||
* wenn der Code unterwegs sichtbar wird — etwa in einem Proxy-Protokoll.
|
||||
*/
|
||||
export async function GET(): Promise<Response> {
|
||||
export async function GET(request: NextRequest): Promise<Response> {
|
||||
try {
|
||||
if (!isAuthEnabled()) {
|
||||
throw new QrRequestError(
|
||||
@@ -41,6 +43,10 @@ export async function GET(): Promise<Response> {
|
||||
);
|
||||
}
|
||||
|
||||
// Vor allem anderen: Kommt der Browser über die Adresse, auf die
|
||||
// BASE_URL zeigt? Sonst geht der Vorgang auf dem Rückweg verloren.
|
||||
assertHostMatchesBaseUrl(request.headers);
|
||||
|
||||
const config = getOidcConfig();
|
||||
const meta = await discover(config.issuer);
|
||||
|
||||
|
||||
@@ -96,6 +96,58 @@ export function redirectUri(): string {
|
||||
return `${getBaseUrl()}/api/auth/oidc/callback`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob der Browser über dieselbe Adresse kommt, auf die `BASE_URL` zeigt.
|
||||
*
|
||||
* Warum das zwingend ist: Der Aufruf setzt einen Cookie mit dem laufenden
|
||||
* Vorgang — und zwar auf dem Host, über den der Browser gerade da ist. Die
|
||||
* Rückkehradresse für den Anbieter kommt dagegen aus `BASE_URL`. Weichen die
|
||||
* beiden voneinander ab, schickt der Anbieter den Browser auf einen anderen
|
||||
* Host, der Cookie geht dabei verloren, und die Anmeldung endet mit
|
||||
* „Der Anmeldevorgang ist abgelaufen“ — einer Meldung, die die eigentliche
|
||||
* Ursache gut versteckt.
|
||||
*
|
||||
* Deshalb hier eine frühe Prüfung mit einer Meldung, die beide Adressen nennt.
|
||||
*/
|
||||
export function assertHostMatchesBaseUrl(headers: Headers): void {
|
||||
const angekommen = (
|
||||
headers.get("x-forwarded-host") ??
|
||||
headers.get("host") ??
|
||||
""
|
||||
)
|
||||
.split(",")[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (!angekommen) return;
|
||||
|
||||
let erwartet = "";
|
||||
try {
|
||||
erwartet = new URL(getBaseUrl()).host.toLowerCase();
|
||||
} catch {
|
||||
return; // Unbrauchbare BASE_URL — dafür gibt es eigene Meldungen.
|
||||
}
|
||||
|
||||
if (angekommen === erwartet) return;
|
||||
|
||||
// Ins Protokoll gehören beide Adressen — dort liest nur mit, wer den Server
|
||||
// bedient. Auf dem Bildschirm steht bewusst nur die, über die der Aufruf
|
||||
// ohnehin kam: Die Anmeldeseite sieht jeder, der die Adresse kennt, und die
|
||||
// interne Adresse der Installation geht Fremde nichts an.
|
||||
console.error(
|
||||
`OIDC: Host-Abweichung — Aufruf über "${angekommen}", BASE_URL zeigt auf "${erwartet}".`,
|
||||
);
|
||||
|
||||
throw new QrRequestError(
|
||||
`Die Anmeldung über den Anbieter ist für „${angekommen}" nicht eingerichtet. ` +
|
||||
`In der Konfiguration steht eine andere Adresse, deshalb ginge der ` +
|
||||
`Anmeldevorgang auf dem Rückweg verloren. Wer die Anwendung betreibt, ` +
|
||||
`findet den erwarteten Wert unter BASE_URL — und die Einzelheiten im ` +
|
||||
`Protokoll des Containers.`,
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
type Discovery = {
|
||||
issuer: string;
|
||||
authorization_endpoint: string;
|
||||
|
||||
Reference in New Issue
Block a user