Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 26 additions & 12 deletions packages/authjs-nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ModuleOptions>({
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<ModuleOptions, "secret">

// 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<ModuleOptions, "secret">

// 3. Add composables
addImports([{ name: "useAuth", from: resolve("./runtime/composables/useAuth") }])
Expand Down Expand Up @@ -72,8 +86,8 @@ export default defineNuxtModule<ModuleOptions>({
// 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.`)
}
})
16 changes: 11 additions & 5 deletions packages/authjs-nuxt/src/nuxt-types.d.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import type { ModuleOptions } from "./module";

declare module "@nuxt/schema" {
interface PublicRuntimeConfig {
authJs: ModuleOptions
}
interface RuntimeConfig {
authJs: Pick<ModuleOptions, 'secret'>
}
interface PublicRuntimeConfig {
authJs: Omit<ModuleOptions, 'secret'>
}
}

declare module "nuxt/schema" {
interface RuntimeConfig {
authJs: Pick<ModuleOptions, 'secret'>
}
interface PublicRuntimeConfig {
authJs: ModuleOptions
authJs: Omit<ModuleOptions, 'secret'>
}
}

export {}
export { }
9 changes: 5 additions & 4 deletions packages/authjs-nuxt/src/runtime/composables/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -10,13 +11,13 @@ export function useAuth() {
const session = useState("auth:session", () => null) as Ref<Session | null>
const cookies = useState("auth:cookies", () => ({})) as Ref<Record<string, string> | 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<User | null>
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) => {
Expand Down
17 changes: 9 additions & 8 deletions packages/authjs-nuxt/src/runtime/lib/server.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -65,16 +66,16 @@ export async function getServerSession(
*/
export async function getServerToken(event: H3Event, options: AuthConfig, runtimeConfig?: Partial<RuntimeConfig>) {
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<string, string>
},
// 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)
}
Expand Down
83 changes: 62 additions & 21 deletions packages/authjs-nuxt/src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<RuntimeConfig>) {
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<RuntimeConfig>) {
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<RuntimeConfig>) {
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
}
Expand All @@ -28,35 +42,62 @@ export function checkOrigin(request: Request, runtimeConfig: Partial<RuntimeConf
if (process.env.NODE_ENV === "development") return
if (request.method !== "POST") return // Only check post requests
const requestOrigin = request.headers.get("Origin")
const serverOrigin = runtimeConfig.public?.authJs?.baseUrl
if (serverOrigin !== requestOrigin)
throw new Error("CSRF protected")
const serverOrigin = getConfigBaseUrl(runtimeConfig)
if (serverOrigin !== requestOrigin) throw new Error("CSRF protected")
}

export function mergeCookieObject(
headers: Record<string, string>,
cookieName: string
) {
return Object.entries(headers)
.filter(([k]) => k.startsWith(cookieName))
.flatMap(([, v]) => v)
.join("")
}

export function makeSessionCookie(headers: Record<string, string> | 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<string, string>) {
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<Record<string, string>>(
(sum, [, value]) => ({ ...sum, ...makeCookiesFromCookieString(value) }),
{}
)
}

/**
* This should be a function in H3
* @param headers RequestHeaders
* @returns Headers
*/
export function makeNativeHeaders(headers: RequestHeaders) {
const nativeHeaders = new Headers()
Object.entries(headers).forEach(([key, value]) => {
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<string, string>
) {
return makeNativeHeaders(headers, ([key, value]) => ["set-cookie", `${key}=${value}`])
}

/**
Expand All @@ -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 })
}
29 changes: 26 additions & 3 deletions packages/authjs-nuxt/test/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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 } } })

Expand All @@ -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", () => {
Expand All @@ -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()
Expand Down