feat: add optional OIDC authentication
This commit is contained in:
101
lib/auth-config.ts
Normal file
101
lib/auth-config.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export interface AuthEnv {
|
||||
OIDC_ISSUER?: string;
|
||||
OIDC_CLIENT_ID?: string;
|
||||
OIDC_CLIENT_SECRET?: string;
|
||||
OIDC_PROVIDER_NAME?: string;
|
||||
AUTH_SECRET?: string;
|
||||
}
|
||||
|
||||
export interface AuthMode {
|
||||
enabled: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface OidcProfile {
|
||||
sub: unknown;
|
||||
name?: unknown;
|
||||
preferred_username?: unknown;
|
||||
email?: unknown;
|
||||
picture?: unknown;
|
||||
}
|
||||
|
||||
const REQUIRED_AUTH_KEYS = [
|
||||
"OIDC_ISSUER",
|
||||
"OIDC_CLIENT_ID",
|
||||
"OIDC_CLIENT_SECRET",
|
||||
"AUTH_SECRET",
|
||||
] as const;
|
||||
|
||||
const OIDC_KEYS = [
|
||||
"OIDC_ISSUER",
|
||||
"OIDC_CLIENT_ID",
|
||||
"OIDC_CLIENT_SECRET",
|
||||
] as const;
|
||||
|
||||
const STATIC_FILE_PATTERN = /\.(?:ico|svg|png|jpg|jpeg|gif|webp|css|js|map|txt|xml|json)$/i;
|
||||
|
||||
export function getAuthMode(env: AuthEnv = process.env): AuthMode {
|
||||
const hasAnyOidcConfig = OIDC_KEYS.some((key) => Boolean(trimEnv(env[key])));
|
||||
if (!hasAnyOidcConfig) return { enabled: false, error: null };
|
||||
|
||||
const missing = REQUIRED_AUTH_KEYS.filter((key) => !trimEnv(env[key]));
|
||||
if (missing.length > 0) {
|
||||
return {
|
||||
enabled: false,
|
||||
error: `Missing required auth environment variables: ${missing.join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
return { enabled: true, error: null };
|
||||
}
|
||||
|
||||
export function isAuthEnabled(env: AuthEnv = process.env): boolean {
|
||||
return getAuthMode(env).enabled;
|
||||
}
|
||||
|
||||
export function isAuthRoute(pathname: string): boolean {
|
||||
return pathname === "/api/auth" || pathname.startsWith("/api/auth/");
|
||||
}
|
||||
|
||||
export function isProtectedPath(pathname: string): boolean {
|
||||
if (isAuthRoute(pathname)) return false;
|
||||
if (pathname.startsWith("/_next/")) return false;
|
||||
if (pathname === "/favicon.ico" || pathname === "/robots.txt" || pathname === "/sitemap.xml") return false;
|
||||
if (STATIC_FILE_PATTERN.test(pathname)) return false;
|
||||
|
||||
return pathname === "/" || pathname.startsWith("/api/") || pathname.startsWith("/");
|
||||
}
|
||||
|
||||
export function getOidcProviderName(env: AuthEnv = process.env): string {
|
||||
return trimEnv(env.OIDC_PROVIDER_NAME) || "OIDC";
|
||||
}
|
||||
|
||||
export function getRequiredAuthSecret(env: AuthEnv = process.env): string {
|
||||
const secret = trimEnv(env.AUTH_SECRET);
|
||||
if (!secret) throw new Error("Missing required auth environment variable: AUTH_SECRET");
|
||||
return secret;
|
||||
}
|
||||
|
||||
export function mapOidcProfile(profile: OidcProfile) {
|
||||
const subject = String(profile.sub);
|
||||
const name = firstString(profile.name, profile.preferred_username, profile.email, subject);
|
||||
|
||||
return {
|
||||
id: subject,
|
||||
name,
|
||||
email: typeof profile.email === "string" ? profile.email : null,
|
||||
image: typeof profile.picture === "string" ? profile.picture : null,
|
||||
};
|
||||
}
|
||||
|
||||
function trimEnv(value: string | undefined): string {
|
||||
return value?.trim() ?? "";
|
||||
}
|
||||
|
||||
function firstString(...values: unknown[]): string {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim()) return value;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
Reference in New Issue
Block a user