import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; import type { OIDCConfig } from "@auth/core/providers"; import type { Profile } from "@auth/core/types"; import { customFetch } from "next-auth"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { env } from "../../env"; import { createRedirectUri } from "../../redirect"; export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig => ({ id: "oidc", name: env.AUTH_OIDC_CLIENT_NAME, type: "oidc", clientId: env.AUTH_OIDC_CLIENT_ID, clientSecret: env.AUTH_OIDC_CLIENT_SECRET, issuer: env.AUTH_OIDC_ISSUER, allowDangerousEmailAccountLinking: env.AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING, authorization: { params: { scope: env.AUTH_OIDC_SCOPE_OVERWRITE, // We fallback to https as generally oidc providers require https redirect_uri: createRedirectUri(headers, "/api/auth/callback/oidc", "https"), }, }, token: { // Providers like fusionauth may return www-authenticate which results in an error // https://github.com/nextauthjs/next-auth/issues/8745 // https://github.com/homarr-labs/homarr/issues/2690 conform: (response: Response) => { if (response.status === 401) return response; const newHeaders = Array.from(response.headers.entries()) .filter(([key]) => key.toLowerCase() !== "www-authenticate") .reduce((headers, [key, value]) => { headers.append(key, value); return headers; }, new Headers()); return new Response(response.body, { status: response.status, statusText: response.statusText, headers: newHeaders, }); }, }, // idToken false forces the use of the userinfo endpoint // Userinfo endpoint is required for authelia since v4.39 // See https://github.com/homarr-labs/homarr/issues/2635 idToken: !env.AUTH_OIDC_FORCE_USERINFO, profile(profile) { if (!profile.sub) { throw new Error(`OIDC provider did not return a sub property='${Object.keys(profile).join(",")}'`); } const name = extractProfileName(profile); if (!name) { throw new Error(`OIDC provider did not return a name properties='${Object.keys(profile).join(",")}'`); } return { id: profile.sub, name, email: profile.email, provider: "oidc", }; }, // The type for fetch is not identical, but for what we need it it's okay to not be an 1:1 match // See documentation https://authjs.dev/guides/corporate-proxy?framework=next-js // @ts-expect-error `undici` has a `duplex` option [customFetch]: fetchWithTrustedCertificatesAsync, }); export const extractProfileName = (profile: Profile) => { if (!env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE) { // Use the name as the username if the preferred_username is an email address return profile.preferred_username?.includes("@") ? profile.name : profile.preferred_username; } return profile[env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE as keyof typeof profile] as string; };