Add optional OIDC authentication via Better Auth genericOAuth plugin

Support self-hosted OIDC providers (Authentik, Authelia, Keycloak, etc.)
configured entirely via environment variables. Uses Better Auth's
hooks.before to gate email/password sign-up at the endpoint level, and
disables emailAndPassword entirely when DISABLE_PASSWORD_LOGIN is set.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 13:39:15 -05:00
co-authored by Claude Opus 4.6
parent e19ac78b39
commit fb785645f8
9 changed files with 320 additions and 135 deletions
+15 -1
View File
@@ -1,9 +1,23 @@
import { AuthForm } from "@/components/auth-form";
import {
getOidcProviderName,
isOidcConfigured,
isPasswordLoginDisabled,
} from "@/lib/config";
export default function LoginPage() {
const oidcEnabled = isOidcConfigured();
return (
<div className="flex min-h-[80vh] items-center justify-center px-4">
<AuthForm mode="login" />
<AuthForm
mode="login"
authConfig={{
oidcEnabled,
oidcProviderName: oidcEnabled ? getOidcProviderName() : null,
passwordLoginDisabled: isPasswordLoginDisabled(),
}}
/>
</div>
);
}
+15 -2
View File
@@ -4,17 +4,30 @@ import { IconLock } from "@tabler/icons-react";
import { motion } from "motion/react";
import Link from "next/link";
import { useEffect, useState } from "react";
import type { AuthConfig } from "@/components/auth-form";
import { AuthForm } from "@/components/auth-form";
export default function RegisterPage() {
const [registrationOpen, setRegistrationOpen] = useState<boolean | null>(
null,
);
const [authConfig, setAuthConfig] = useState<AuthConfig>({
oidcEnabled: false,
oidcProviderName: null,
passwordLoginDisabled: false,
});
useEffect(() => {
fetch("/api/registration/status")
.then((res) => res.json())
.then((data) => setRegistrationOpen(data.registrationOpen))
.then((data) => {
setRegistrationOpen(data.registrationOpen);
setAuthConfig({
oidcEnabled: data.oidcEnabled ?? false,
oidcProviderName: data.oidcProviderName ?? null,
passwordLoginDisabled: data.passwordLoginDisabled ?? false,
});
})
.catch(() => setRegistrationOpen(false));
}, []);
@@ -63,7 +76,7 @@ export default function RegisterPage() {
return (
<div className="flex min-h-[80vh] items-center justify-center px-4">
<AuthForm mode="register" />
<AuthForm mode="register" authConfig={authConfig} />
</div>
);
}
+11 -2
View File
@@ -1,7 +1,16 @@
import { NextResponse } from "next/server";
import {
getOidcProviderName,
isOidcConfigured,
isPasswordLoginDisabled,
} from "@/lib/config";
import { isRegistrationOpen } from "@/lib/services/settings";
export async function GET() {
const registrationOpen = await isRegistrationOpen();
return NextResponse.json({ registrationOpen });
return NextResponse.json({
registrationOpen: await isRegistrationOpen(),
oidcEnabled: isOidcConfigured(),
oidcProviderName: isOidcConfigured() ? getOidcProviderName() : null,
passwordLoginDisabled: isPasswordLoginDisabled(),
});
}