diff --git a/.vscode/settings.json b/.vscode/settings.json index 92d34328..eb1a35b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "prettier.enable": false, "editor.formatOnSave": false, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "workbench.colorCustomizations": {}, "inline-bookmarks.view.showVisibleFilesOnly": false diff --git a/packages/authjs-nuxt/src/module.ts b/packages/authjs-nuxt/src/module.ts index f272c5eb..14ad1fe3 100644 --- a/packages/authjs-nuxt/src/module.ts +++ b/packages/authjs-nuxt/src/module.ts @@ -2,35 +2,49 @@ import { addImports, addPlugin, addRouteMiddleware, addTypeTemplate, createResol import { defu } from "defu" import { configKey } from "./runtime/utils" -const NAME = "@auth/nuxt" +const PACKAGE_NAME = "@auth/nuxt" export interface ModuleOptions { + secret?: string + baseUrl?: string verifyClientOnEveryRequest?: boolean guestRedirectTo?: string authenticatedRedirectTo?: string - baseUrl: string } export default defineNuxtModule({ meta: { - name: NAME, + name: PACKAGE_NAME, configKey }, defaults: { + secret: "", + baseUrl: "", verifyClientOnEveryRequest: true, guestRedirectTo: "/", - authenticatedRedirectTo: "/", - baseUrl: "" + authenticatedRedirectTo: "/" }, - setup(userOptions, nuxt) { - const logger = useLogger(NAME) + setup(moduleOptions, nuxt) { + const logger = useLogger(PACKAGE_NAME) const { resolve } = createResolver(import.meta.url) - logger.info(`Adding ${NAME} module...`, userOptions) + logger.info(`Adding ${PACKAGE_NAME} module...`) // 1. Set up runtime configuration - const options = defu(nuxt.options.runtimeConfig.public[configKey], userOptions) - nuxt.options.runtimeConfig.public[configKey] = options + + // Private runtime config + nuxt.options.runtimeConfig[configKey] = defu( + nuxt.options.runtimeConfig[configKey], + { secret: moduleOptions.secret } + ) as Pick + + // Public runtime config + nuxt.options.runtimeConfig.public[configKey] = defu(nuxt.options.runtimeConfig.public[configKey], { + baseUrl: moduleOptions.baseUrl, + verifyClientOnEveryRequest: moduleOptions.verifyClientOnEveryRequest, + guestRedirectTo: moduleOptions.guestRedirectTo, + authenticatedRedirectTo: moduleOptions.authenticatedRedirectTo + }) as Omit // 3. Add composables addImports([{ name: "useAuth", from: resolve("./runtime/composables/useAuth") }]) @@ -72,8 +86,8 @@ export default defineNuxtModule({ // 6. Add middlewares addRouteMiddleware({ name: "auth", path: resolve("./runtime/middleware/auth") }) addRouteMiddleware({ name: "guest-only", path: resolve("./runtime/middleware/guest-only") }) - addRouteMiddleware({ name: "client-auth", path: resolve("./runtime/middleware/client-auth"), global: options.verifyClientOnEveryRequest }) + addRouteMiddleware({ name: "client-auth", path: resolve("./runtime/middleware/client-auth"), global: nuxt.options.runtimeConfig.public[configKey].verifyClientOnEveryRequest }) - logger.success(`Added ${NAME} module successfully.`) + logger.success(`Added ${PACKAGE_NAME} module successfully.`) } }) diff --git a/packages/authjs-nuxt/src/nuxt-types.d.ts b/packages/authjs-nuxt/src/nuxt-types.d.ts index abf3f6c6..ff689b68 100644 --- a/packages/authjs-nuxt/src/nuxt-types.d.ts +++ b/packages/authjs-nuxt/src/nuxt-types.d.ts @@ -1,15 +1,21 @@ import type { ModuleOptions } from "./module"; declare module "@nuxt/schema" { - interface PublicRuntimeConfig { - authJs: ModuleOptions - } + interface RuntimeConfig { + authJs: Pick + } + interface PublicRuntimeConfig { + authJs: Omit + } } declare module "nuxt/schema" { + interface RuntimeConfig { + authJs: Pick + } interface PublicRuntimeConfig { - authJs: ModuleOptions + authJs: Omit } } -export {} +export { } diff --git a/packages/authjs-nuxt/src/runtime/composables/useAuth.ts b/packages/authjs-nuxt/src/runtime/composables/useAuth.ts index 383b7423..0f2db6e1 100644 --- a/packages/authjs-nuxt/src/runtime/composables/useAuth.ts +++ b/packages/authjs-nuxt/src/runtime/composables/useAuth.ts @@ -2,6 +2,7 @@ import type { Session, User } from "@auth/core/types" import { produce } from "immer" import { type ComputedRef, type Ref, computed, readonly, watch } from "vue" import { getProviders, signIn, signOut } from "../lib/client" +import { makeSessionCookie } from "../utils" import { useState } from "#imports" // @note the `as Type` statements are necessary for rollup to generate the correct types @@ -10,13 +11,13 @@ export function useAuth() { const session = useState("auth:session", () => null) as Ref const cookies = useState("auth:cookies", () => ({})) as Ref | null> const status = useState("auth:session:status", () => "unauthenticated") as Ref<"loading" | "authenticated" | "unauthenticated" | "error"> - const sessionToken = computed(() => cookies.value?.["next-auth.session-token"] ?? "") + const sessionToken = computed(() => makeSessionCookie(cookies.value)) const user = computed(() => session.value?.user ?? null) as ComputedRef watch(session, (newSession: Session | null) => { if (newSession === null) - return (status.value = "unauthenticated") - if (Object.keys(newSession).length) - return (status.value = "authenticated") + status.value = "unauthenticated" + else if (Object.keys(newSession).length) + status.value = "authenticated" }) const updateSession = (u: (() => unknown) | Session | null) => { diff --git a/packages/authjs-nuxt/src/runtime/lib/server.ts b/packages/authjs-nuxt/src/runtime/lib/server.ts index effe8544..4b74d5a8 100644 --- a/packages/authjs-nuxt/src/runtime/lib/server.ts +++ b/packages/authjs-nuxt/src/runtime/lib/server.ts @@ -1,10 +1,10 @@ -import type { RuntimeConfig } from "nuxt/schema" import { Auth, skipCSRFCheck } from "@auth/core" +import { getToken } from "@auth/core/jwt" +import type { AuthConfig, Session } from "@auth/core/types" import type { H3Event } from "h3" import { eventHandler, getRequestHeaders, getRequestURL } from "h3" -import type { AuthConfig, Session } from "@auth/core/types" -import { getToken } from "@auth/core/jwt" -import { checkOrigin, getAuthJsSecret, getRequestFromEvent, getServerOrigin, makeCookiesFromCookieString } from "../utils" +import type { RuntimeConfig } from "nuxt/schema" +import { checkOrigin, getAuthJsSecret, getRequestFromEvent, getServerOrigin, makeCookiesFromHeaders } from "../utils" if (!globalThis.crypto) { // eslint-disable-next-line no-console @@ -27,6 +27,7 @@ if (!globalThis.crypto) { */ export function NuxtAuthHandler(options: AuthConfig, runtimeConfig: RuntimeConfig) { return eventHandler(async (event) => { + options.secret ||= getAuthJsSecret(runtimeConfig) options.trustHost ??= true options.skipCSRFCheck = skipCSRFCheck const request = await getRequestFromEvent(event) @@ -65,16 +66,16 @@ export async function getServerSession( */ export async function getServerToken(event: H3Event, options: AuthConfig, runtimeConfig?: Partial) { const response = await getServerSessionResponse(event, options) - const cookies = Object.fromEntries(response.headers.entries()) - const parsedCookies = makeCookiesFromCookieString(cookies["set-cookie"]) + const headers = response.headers + const cookies = makeCookiesFromHeaders(headers) const parameters = { req: { - cookies: parsedCookies, + cookies, headers: response.headers as unknown as Record }, // see https://github.com/nextauthjs/next-auth/blob/a79774f6e890b492ae30201f24b3f7024d0d7c9d/packages/core/src/jwt.ts secureCookie: getServerOrigin(event, runtimeConfig).startsWith("https://"), - secret: getAuthJsSecret(options) + secret: options.secret || getAuthJsSecret(runtimeConfig) } return getToken(parameters) } diff --git a/packages/authjs-nuxt/src/runtime/utils.ts b/packages/authjs-nuxt/src/runtime/utils.ts index 42059f30..05034fb0 100644 --- a/packages/authjs-nuxt/src/runtime/utils.ts +++ b/packages/authjs-nuxt/src/runtime/utils.ts @@ -1,7 +1,6 @@ -import type { AuthConfig } from "@auth/core" import { parse } from "cookie-es" import type { H3Event, RequestHeaders } from "h3" -import { getMethod, getRequestHeaders, getRequestURL, readRawBody } from "h3" +import { getRequestHeaders, getRequestURL, readRawBody } from "h3" import type { RuntimeConfig } from "@nuxt/schema" export const configKey = "authJs" as const @@ -10,16 +9,31 @@ export const configKey = "authJs" as const * Get the AuthJS secret. For internal use only. * @returns The secret used to sign the JWT Token */ -export function getAuthJsSecret(options: AuthConfig) { - const secret = options?.secret || process.env.NUXT_NEXTAUTH_SECRET || process.env.NEXTAUTH_SECRET || process.env.AUTH_SECRET +export function getAuthJsSecret(runtimeConfig?: Partial) { + const secret = runtimeConfig?.authJs?.secret + || process.env.NUXT_NEXTAUTH_SECRET + || process.env.NUXT_AUTH_JS_SECRET + || process.env.NEXTAUTH_SECRET + || process.env.AUTH_SECRET if (!secret) throw new Error("[authjs-nuxt] No secret found, please set a secret in your [...].ts handler or use environment variables") return secret } +export function getConfigBaseUrl(runtimeConfig?: Partial) { + return ( + runtimeConfig?.public?.authJs?.baseUrl + || process.env.NUXT_NEXTAUTH_URL + || process.env.NUXT_AUTH_JS_BASE_URL + || process.env.NEXTAUTH_URL + || process.env.AUTH_ORIGIN + || "" + ) +} + export function getServerOrigin(event: H3Event, runtimeConfig?: Partial) { const requestOrigin = getRequestHeaders(event).Origin - const serverOrigin = runtimeConfig?.public?.authJs?.baseUrl ?? "" - const origin = requestOrigin ?? serverOrigin.length > 0 ? serverOrigin : process.env.AUTH_ORIGIN + const serverOrigin = getConfigBaseUrl(runtimeConfig) + const origin = requestOrigin || serverOrigin if (!origin) throw new Error("No Origin found ...") return origin } @@ -28,22 +42,40 @@ export function checkOrigin(request: Request, runtimeConfig: Partial, + cookieName: string +) { + return Object.entries(headers) + .filter(([k]) => k.includes(cookieName)) + .flatMap(([, v]) => v) + .join("") +} + +export function makeSessionCookie(headers: Record | null) { + if (!headers) return "" + return mergeCookieObject(headers, "next-auth.session-token") } export function makeCookiesFromCookieString(cookieString: string | null) { if (!cookieString) return {} return Object.fromEntries( - Object.entries(parse(cookieString)).filter(([k]) => k.includes("next-auth")) + Object.entries(parse(cookieString)) + .filter(([k]) => k.includes("next-auth")) ) } -export function makeNativeHeadersFromCookieObject(headers: Record) { - const nativeHeaders = new Headers(Object.entries(headers) - .map(([key, value]) => ["set-cookie", `${key}=${value}`]) as HeadersInit) - return nativeHeaders +export function makeCookiesFromHeaders(headers: Headers) { + return Array.from(headers) + .filter(([key]) => key === "set-cookie") + .reduce>( + (sum, [, value]) => ({ ...sum, ...makeCookiesFromCookieString(value) }), + {} + ) } /** @@ -51,12 +83,21 @@ export function makeNativeHeadersFromCookieObject(headers: Record { - if (value) nativeHeaders.append(key, value) - }) - return nativeHeaders +export function makeNativeHeaders( + headers: RequestHeaders, + mapFn = ([key, value]: [string, string]): [string, string] => [key, value] +) { + return new Headers( + Object.entries(headers) + .filter(([, value]) => !!value) + .map(([key, value]) => mapFn([key, value!])) + ) +} + +export function makeNativeHeadersFromCookieObject( + headers: Record +) { + return makeNativeHeaders(headers, ([key, value]) => ["set-cookie", `${key}=${value}`]) } /** @@ -66,7 +107,7 @@ export function makeNativeHeaders(headers: RequestHeaders) { */ export async function getRequestFromEvent(event: H3Event) { const url = new URL(getRequestURL(event)) - const method = getMethod(event) + const method = event.method const body = method === "POST" ? await readRawBody(event) : undefined return new Request(url, { headers: getRequestHeaders(event) as any, method, body }) } diff --git a/packages/authjs-nuxt/test/auth.test.ts b/packages/authjs-nuxt/test/auth.test.ts index c684c087..52a7c639 100644 --- a/packages/authjs-nuxt/test/auth.test.ts +++ b/packages/authjs-nuxt/test/auth.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest" -import { checkOrigin, makeCookiesFromCookieString, makeNativeHeaders, makeNativeHeadersFromCookieObject } from "../src/runtime/utils" +import { checkOrigin, makeCookiesFromCookieString, makeCookiesFromHeaders, makeNativeHeaders, makeNativeHeadersFromCookieObject, mergeCookieObject } from "../src/runtime/utils" const mockRuntimeConfig = (baseUrl = "https://example.com") => ({ public: { authJs: { baseUrl } } }) @@ -11,11 +11,12 @@ describe("all", () => { expect(headers instanceof Headers).toBe(true) }) - it("can transform Request headers into regular headers", () => { + it("can transform Request headers into native headers", () => { const headers = { hello: "world", hi: "there" } const nativeHeaders = makeNativeHeaders(headers) expect(nativeHeaders.get("hello")).toBe("world") expect(nativeHeaders.get("hi")).toBe("there") + expect(nativeHeaders instanceof Headers).toBe(true) }) it("can make cookies from cookie string", () => { @@ -25,13 +26,35 @@ describe("all", () => { expect(cookies.hi).toBeUndefined() }) + it("can make cookies from native headers", () => { + const headers = new Headers() + headers.append("set-cookie", "next-auth.hello=world") + headers.append("set-cookie", "next-auth.hi=there") + const cookies = makeCookiesFromHeaders(headers) + expect(cookies["next-auth.hello"]).toBe("world") + expect(cookies["next-auth.hi"]).toBe("there") + }) + + it("can handle splitted cookies from native headers", () => { + const headers = new Headers() + headers.append("set-cookie", "next-auth.greet.0=hello") + headers.append("set-cookie", "next-auth.greet.1=world") + headers.append("set-cookie", "hi=there") + const cookies = makeCookiesFromHeaders(headers) + const greet = mergeCookieObject(cookies, "next-auth.greet") + expect(cookies["next-auth.greet.0"]).toBe("hello") + expect(cookies["next-auth.greet.1"]).toBe("world") + expect(greet).toBe("helloworld") + expect(cookies.hi).toBeUndefined() + }) + it("throws an error if the origin header is not set", () => { const url = "https://example.com" expect(() => checkOrigin(new Request(url, { method: "POST" }), mockRuntimeConfig(url))) .toThrowError("CSRF protected") }) - it("is undefined is the Origin is checked ", () => { + it("is undefined if the Origin is checked", () => { const url = "https://example.com" expect(checkOrigin(new Request(url, { method: "POST", headers: { Origin: url } }), mockRuntimeConfig(url))) .toBeUndefined()