mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 02:45:39 -04:00
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:
+53
-10
@@ -1,8 +1,13 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { admin } from "better-auth/plugins";
|
||||
import { APIError, createAuthMiddleware } from "better-auth/api";
|
||||
import { admin, genericOAuth } from "better-auth/plugins";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import {
|
||||
isOidcAutoRegisterEnabled,
|
||||
isOidcConfigured,
|
||||
isPasswordLoginDisabled,
|
||||
} from "@/lib/config";
|
||||
import { db } from "@/lib/db/client";
|
||||
import {
|
||||
getUserCount,
|
||||
@@ -10,23 +15,66 @@ import {
|
||||
setSetting,
|
||||
} from "@/lib/services/settings";
|
||||
|
||||
const oidcPlugin = isOidcConfigured()
|
||||
? [
|
||||
genericOAuth({
|
||||
config: [
|
||||
{
|
||||
providerId: "oidc",
|
||||
clientId: process.env.OIDC_CLIENT_ID ?? "",
|
||||
clientSecret: process.env.OIDC_CLIENT_SECRET ?? "",
|
||||
discoveryUrl: `${process.env.OIDC_ISSUER_URL}/.well-known/openid-configuration`,
|
||||
scopes: ["openid", "email", "profile"],
|
||||
pkce: true,
|
||||
disableImplicitSignUp: !isOidcAutoRegisterEnabled(),
|
||||
mapProfileToUser: (profile) => ({
|
||||
name: profile.name || profile.preferred_username || profile.email,
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "sqlite",
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
enabled: !isPasswordLoginDisabled(),
|
||||
},
|
||||
plugins: [admin()],
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: ["oidc"],
|
||||
},
|
||||
},
|
||||
plugins: [admin(), ...oidcPlugin],
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: () => uuid(),
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
before: createAuthMiddleware(async (ctx) => {
|
||||
// Block email/password sign-up when registration is closed.
|
||||
// This is endpoint-level so it doesn't affect OIDC user creation
|
||||
// (which is gated by the genericOAuth plugin's disableImplicitSignUp).
|
||||
if (ctx.path === "/sign-up/email") {
|
||||
const open = await isRegistrationOpen();
|
||||
if (!open) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Registration is currently closed",
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
before: async (userData) => {
|
||||
// First user becomes admin regardless of auth method
|
||||
const userCount = await getUserCount();
|
||||
if (userCount === 0) {
|
||||
return {
|
||||
@@ -36,15 +84,10 @@ export const auth = betterAuth({
|
||||
},
|
||||
};
|
||||
}
|
||||
const open = await isRegistrationOpen();
|
||||
if (!open) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Registration is currently closed",
|
||||
});
|
||||
}
|
||||
return { data: userData };
|
||||
},
|
||||
after: async () => {
|
||||
// Auto-close registration after first user
|
||||
const userCount = await getUserCount();
|
||||
if (userCount === 1) {
|
||||
await setSetting("registrationOpen", "false");
|
||||
|
||||
Reference in New Issue
Block a user