feat: cross-subdomainCookies plugin

This commit is contained in:
Bereket Engida
2024-09-28 13:00:57 +03:00
parent 71913c49f6
commit fe9ef0c5d2
6 changed files with 117 additions and 13 deletions

View File

@@ -1,5 +1,10 @@
import { betterAuth } from "better-auth"; 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 { reactInvitationEmail } from "./email/invitation";
import { LibsqlDialect } from "@libsql/kysely-libsql"; import { LibsqlDialect } from "@libsql/kysely-libsql";
import { github, google } from "better-auth/social-providers"; import { github, google } from "better-auth/social-providers";
@@ -75,6 +80,7 @@ export const auth = betterAuth({
}, },
}), }),
passkey(), passkey(),
crossSubdomain(),
], ],
socialProviders: { socialProviders: {
github: { github: {

View File

@@ -174,6 +174,17 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
async onRequest(req) { async onRequest(req) {
return onRequestRateLimit(req, ctx); 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) { onError(e) {
const log = options.logger?.verboseLogging ? logger : undefined; const log = options.logger?.verboseLogging ? logger : undefined;
if (options.logger?.disabled !== true) { if (options.logger?.disabled !== true) {

View File

@@ -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;
};

View File

@@ -7,3 +7,4 @@ export * from "../types/plugins";
export * from "../api/call"; export * from "../api/call";
export * from "../utils/hide-metadata"; export * from "../utils/hide-metadata";
export * from "./magic-link"; export * from "./magic-link";
export * from "./cross-subdomain";

View File

@@ -1,16 +1,17 @@
import type { ContextTools } from "better-call"; import type { ContextTools } from "better-call";
import type { AuthContext } from "../init"; import type { AuthContext } from "../init";
export type HookEndpointContext = ContextTools & { export type HookEndpointContext<C extends Record<string, any> = {}> =
context: AuthContext; ContextTools & {
} & { context: AuthContext & C;
body: any; } & {
request?: Request; body: any;
headers?: Headers; request?: Request;
params?: Record<string, string> | undefined; headers?: Headers;
query?: any; params?: Record<string, string> | undefined;
method?: any; query?: any;
}; method?: any;
};
export type GenericEndpointContext = ContextTools & { export type GenericEndpointContext = ContextTools & {
context: AuthContext; context: AuthContext;

View File

@@ -4,6 +4,7 @@ import type { AuthEndpoint } from "../api/call";
import type { FieldAttribute } from "../db/field"; import type { FieldAttribute } from "../db/field";
import type { HookEndpointContext } from "./context"; import type { HookEndpointContext } from "./context";
import type { LiteralString } from "./helper"; import type { LiteralString } from "./helper";
import type { AuthContext } from ".";
export type PluginSchema = { export type PluginSchema = {
[table: string]: { [table: string]: {
@@ -23,6 +24,12 @@ export type BetterAuthPlugin = {
path: string; path: string;
middleware: Endpoint; middleware: Endpoint;
}[]; }[];
onResponse?: (
response: Response,
ctx: AuthContext,
) => Promise<{
response: Response;
} | void>;
hooks?: { hooks?: {
before?: { before?: {
matcher: (context: HookEndpointContext) => boolean; matcher: (context: HookEndpointContext) => boolean;
@@ -33,9 +40,9 @@ export type BetterAuthPlugin = {
after?: { after?: {
matcher: (context: HookEndpointContext) => boolean; matcher: (context: HookEndpointContext) => boolean;
handler: ( handler: (
context: HookEndpointContext & { context: HookEndpointContext<{
returned: EndpointResponse; returned: EndpointResponse;
}, }>,
) => Promise<void | { ) => Promise<void | {
response: EndpointResponse; response: EndpointResponse;
}>; }>;