diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index bb295445..6a968c0d 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -1,5 +1,10 @@ import { betterAuth } from "better-auth"; -import { organization, passkey, twoFactor } from "better-auth/plugins"; +import { + organization, + passkey, + twoFactor, + crossSubdomain, +} from "better-auth/plugins"; import { reactInvitationEmail } from "./email/invitation"; import { LibsqlDialect } from "@libsql/kysely-libsql"; import { github, google } from "better-auth/social-providers"; @@ -75,6 +80,7 @@ export const auth = betterAuth({ }, }), passkey(), + crossSubdomain(), ], socialProviders: { github: { diff --git a/packages/better-auth/src/api/index.ts b/packages/better-auth/src/api/index.ts index bff79010..2a1e57bb 100644 --- a/packages/better-auth/src/api/index.ts +++ b/packages/better-auth/src/api/index.ts @@ -174,6 +174,17 @@ export const router = ( async onRequest(req) { return onRequestRateLimit(req, ctx); }, + async onResponse(res) { + for (const plugin of ctx.options.plugins || []) { + if (plugin.onResponse) { + const response = await plugin.onResponse(res, ctx); + if (response) { + return response.response; + } + } + } + return res; + }, onError(e) { const log = options.logger?.verboseLogging ? logger : undefined; if (options.logger?.disabled !== true) { diff --git a/packages/better-auth/src/plugins/cross-subdomain/index.ts b/packages/better-auth/src/plugins/cross-subdomain/index.ts new file mode 100644 index 00000000..70c379d5 --- /dev/null +++ b/packages/better-auth/src/plugins/cross-subdomain/index.ts @@ -0,0 +1,78 @@ +import { createAuthMiddleware } from "../../api"; +import type { BetterAuthPlugin } from "../../types"; + +interface Options { + /** + * By default, domain name will be extracted from base URL + * you can provide a custom domain name here + */ + domainName?: string; + /** + * List of cookies that should be shared across subdomains + * + * by default, only sessionToken, csrfToken and dontRememberToken + * cookies will be shared across subdomains + */ + eligibleCookies?: string[]; +} + +/** + * This plugin will update the domain of the cookies + * that are eligible to be shared across subdomains + * @param options + * @category Plugins + */ +export const crossSubdomainCookies = (options?: Options) => { + return { + id: "cross-subdomain-cookies", + async onResponse(response, ctx) { + const setCookie = response.headers.get("set-cookie"); + if (!setCookie) return; + const baseURL = ctx.baseURL; + const cookieParts = setCookie.split(";"); + const domain = options?.domainName || new URL(baseURL).hostname; + const authCookies = ctx.authCookies; + const cookieNamesEligibleForDomain = [ + authCookies.sessionToken.name, + authCookies.csrfToken.name, + authCookies.dontRememberToken.name, + ]; + + if ( + !cookieNamesEligibleForDomain.some((name) => setCookie.includes(name)) + ) { + return; + } + + const updatedCookies = cookieParts + .map((part) => { + if ( + !cookieNamesEligibleForDomain.some((name) => + part.toLowerCase().includes(name.toLowerCase()), + ) + ) { + return part; + } + + const trimmedPart = part.trim(); + if (trimmedPart.toLowerCase().startsWith("domain=")) { + return `Domain=${domain}`; + } + if (!trimmedPart.toLowerCase().includes("domain=")) { + return `${trimmedPart}; Domain=${domain}`; + } + return trimmedPart; + }) + .filter( + (part, index, self) => + index === + self.findIndex((p) => p.split(";")[0] === part.split(";")[0]), + ) + .join("; "); + response.headers.set("set-cookie", updatedCookies); + return { + response, + }; + }, + } satisfies BetterAuthPlugin; +}; diff --git a/packages/better-auth/src/plugins/index.ts b/packages/better-auth/src/plugins/index.ts index 8db1e0cd..cd16583e 100644 --- a/packages/better-auth/src/plugins/index.ts +++ b/packages/better-auth/src/plugins/index.ts @@ -7,3 +7,4 @@ export * from "../types/plugins"; export * from "../api/call"; export * from "../utils/hide-metadata"; export * from "./magic-link"; +export * from "./cross-subdomain"; diff --git a/packages/better-auth/src/types/context.ts b/packages/better-auth/src/types/context.ts index 955b415e..6aad1a42 100644 --- a/packages/better-auth/src/types/context.ts +++ b/packages/better-auth/src/types/context.ts @@ -1,16 +1,17 @@ import type { ContextTools } from "better-call"; import type { AuthContext } from "../init"; -export type HookEndpointContext = ContextTools & { - context: AuthContext; -} & { - body: any; - request?: Request; - headers?: Headers; - params?: Record | undefined; - query?: any; - method?: any; -}; +export type HookEndpointContext = {}> = + ContextTools & { + context: AuthContext & C; + } & { + body: any; + request?: Request; + headers?: Headers; + params?: Record | undefined; + query?: any; + method?: any; + }; export type GenericEndpointContext = ContextTools & { context: AuthContext; diff --git a/packages/better-auth/src/types/plugins.ts b/packages/better-auth/src/types/plugins.ts index 7e489886..8a544e64 100644 --- a/packages/better-auth/src/types/plugins.ts +++ b/packages/better-auth/src/types/plugins.ts @@ -4,6 +4,7 @@ import type { AuthEndpoint } from "../api/call"; import type { FieldAttribute } from "../db/field"; import type { HookEndpointContext } from "./context"; import type { LiteralString } from "./helper"; +import type { AuthContext } from "."; export type PluginSchema = { [table: string]: { @@ -23,6 +24,12 @@ export type BetterAuthPlugin = { path: string; middleware: Endpoint; }[]; + onResponse?: ( + response: Response, + ctx: AuthContext, + ) => Promise<{ + response: Response; + } | void>; hooks?: { before?: { matcher: (context: HookEndpointContext) => boolean; @@ -33,9 +40,9 @@ export type BetterAuthPlugin = { after?: { matcher: (context: HookEndpointContext) => boolean; handler: ( - context: HookEndpointContext & { + context: HookEndpointContext<{ returned: EndpointResponse; - }, + }>, ) => Promise;