Prefix all Auth.js cookies (default "homarr") so they no longer collide with other Auth.js apps on the same hostname.
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
import { cookies } from "next/headers";
|
|
import NextAuth from "next-auth";
|
|
import Credentials from "next-auth/providers/credentials";
|
|
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import { db } from "@homarr/db";
|
|
import type { SupportedAuthProvider } from "@homarr/definitions";
|
|
|
|
import { createAdapter } from "./adapter";
|
|
import { createSessionCallback } from "./callbacks";
|
|
import { env } from "./env";
|
|
import { createSignInEventHandler } from "./events";
|
|
import { createCredentialsConfiguration, createLdapConfiguration } from "./providers/credentials/credentials-provider";
|
|
import { EmptyNextAuthProvider } from "./providers/empty/empty-provider";
|
|
import { filterProviders } from "./providers/filter-providers";
|
|
import { OidcProvider } from "./providers/oidc/oidc-provider";
|
|
import { createRedirectUri } from "./redirect";
|
|
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
|
|
|
|
const logger = createLogger({ module: "authConfiguration" });
|
|
|
|
// All Auth.js cookies are prefixed with AUTH_COOKIE_PREFIX (default "homarr") so
|
|
// Homarr's cookie jar is isolated from any other Auth.js/NextAuth app on the
|
|
// same hostname (browsers scope cookies by host, not by port).
|
|
// See https://github.com/homarr-labs/homarr/issues/5773
|
|
const createCookies = (useSecureCookies: boolean) => {
|
|
const prefix = env.AUTH_COOKIE_PREFIX;
|
|
const securePrefix = useSecureCookies ? "__Secure-" : "";
|
|
const hostPrefix = useSecureCookies ? "__Host-" : "";
|
|
return {
|
|
sessionToken: { name: sessionTokenCookieName },
|
|
csrfToken: { name: `${hostPrefix}${prefix}.csrf-token` },
|
|
callbackUrl: { name: `${securePrefix}${prefix}.callback-url` },
|
|
pkceCodeVerifier: { name: `${securePrefix}${prefix}.pkce.cooldown` },
|
|
state: { name: `${securePrefix}${prefix}.state` },
|
|
nonce: { name: `${securePrefix}${prefix}.nonce` },
|
|
};
|
|
};
|
|
|
|
// See why it's unknown in the [...nextauth]/route.ts file
|
|
export const createConfiguration = (
|
|
provider: SupportedAuthProvider | "unknown",
|
|
headers: ReadonlyHeaders | null,
|
|
useSecureCookies: boolean,
|
|
) => {
|
|
const adapter = createAdapter(db, provider);
|
|
return NextAuth({
|
|
logger: {
|
|
error: (error) => {
|
|
// Remove the big error message for failed login attempts
|
|
// as it is not useful for the user.
|
|
if (error.name === "CredentialsSignin") {
|
|
logger.warn("The login attempt of a user was not successful.");
|
|
return;
|
|
}
|
|
|
|
logger.error(error);
|
|
},
|
|
},
|
|
trustHost: true,
|
|
cookies: createCookies(useSecureCookies),
|
|
adapter,
|
|
providers: filterProviders([
|
|
Credentials(createCredentialsConfiguration(db)),
|
|
Credentials(createLdapConfiguration(db)),
|
|
EmptyNextAuthProvider(),
|
|
OidcProvider(headers),
|
|
]),
|
|
callbacks: {
|
|
session: createSessionCallback(db),
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
signIn: async ({ user }) => {
|
|
/**
|
|
* For credentials provider only jwt is supported by default
|
|
* so we have to create the session and set the cookie manually.
|
|
*/
|
|
if (provider !== "credentials" && provider !== "ldap") {
|
|
return true;
|
|
}
|
|
|
|
if (!adapter.createSession || !user.id) {
|
|
return false;
|
|
}
|
|
|
|
const expires = expireDateAfter(env.AUTH_SESSION_EXPIRY_TIME);
|
|
const sessionToken = generateSessionToken();
|
|
await adapter.createSession({
|
|
sessionToken,
|
|
expires,
|
|
userId: user.id,
|
|
});
|
|
|
|
(await cookies()).set(sessionTokenCookieName, sessionToken, {
|
|
path: "/",
|
|
expires: expires,
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: useSecureCookies,
|
|
});
|
|
|
|
return true;
|
|
},
|
|
},
|
|
events: {
|
|
signIn: createSignInEventHandler(db),
|
|
},
|
|
redirectProxyUrl: createRedirectUri(headers, "/api/auth"),
|
|
session: {
|
|
strategy: "database",
|
|
maxAge: env.AUTH_SESSION_EXPIRY_TIME,
|
|
generateSessionToken,
|
|
},
|
|
pages: {
|
|
signIn: "/auth/login",
|
|
error: "/auth/login",
|
|
},
|
|
jwt: {
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
async encode() {
|
|
const cookie = (await cookies()).get(sessionTokenCookieName)?.value;
|
|
return cookie ?? "";
|
|
},
|
|
|
|
decode() {
|
|
return null;
|
|
},
|
|
},
|
|
});
|
|
};
|