mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-19 04:08:43 -05:00
fix(last-login-method): include domain when clearing cross-subdomain cookies (#9319)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
fix(last-login-method): include domain when clearing cross-subdomain cookies
|
||||
@@ -303,7 +303,8 @@ import { lastLoginMethodClient } from "better-auth/client/plugins"
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
lastLoginMethodClient({
|
||||
cookieName: "better-auth.last_used_login_method" // Default: "better-auth.last_used_login_method"
|
||||
cookieName: "better-auth.last_used_login_method", // Default: "better-auth.last_used_login_method"
|
||||
domain: ".example.com" // Required for cross-subdomain cookie clearing
|
||||
})
|
||||
]
|
||||
})
|
||||
@@ -315,6 +316,12 @@ export const authClient = createAuthClient({
|
||||
* Must match the server-side `cookieName` configuration
|
||||
* Default: `"better-auth.last_used_login_method"`
|
||||
|
||||
**domain**: `string`
|
||||
|
||||
* The domain to use when clearing the cookie
|
||||
* Required when using `crossSubDomainCookies` so the client can properly expire the cookie set by the server
|
||||
* Should match the `domain` value in your server's `crossSubDomainCookies` configuration
|
||||
|
||||
### Default Method Resolution
|
||||
|
||||
By default, the plugin tracks these authentication methods:
|
||||
@@ -340,6 +347,23 @@ The plugin automatically inherits cookie settings from Better Auth's centralized
|
||||
|
||||
When you enable `crossSubDomainCookies` or `crossOriginCookies` in your Better Auth config, the plugin will automatically use the same domain, secure, and sameSite settings as your session cookies, ensuring consistent behavior across your application.
|
||||
|
||||
<Callout type="warn">
|
||||
When using `crossSubDomainCookies`, you must pass the `domain` option to `lastLoginMethodClient()` so that `clearLastUsedLoginMethod()` can properly expire the cookie. Without it, browsers will silently ignore the clear request because the domain doesn't match.
|
||||
</Callout>
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
import { lastLoginMethodClient } from "better-auth/client/plugins"
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [
|
||||
lastLoginMethodClient({
|
||||
domain: ".example.com" // Must match server crossSubDomainCookies domain // [!code highlight]
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Custom Provider Tracking
|
||||
|
||||
@@ -11,6 +11,16 @@ export interface LastLoginMethodClientConfig {
|
||||
* @default "better-auth.last_used_login_method"
|
||||
*/
|
||||
cookieName?: string | undefined;
|
||||
/**
|
||||
* Domain for the cookie. Required when using cross-subdomain cookies
|
||||
* so the client can properly clear the cookie set by the server.
|
||||
*
|
||||
* Should match the `domain` value in your server's
|
||||
* `crossSubDomainCookies` configuration.
|
||||
*
|
||||
* @example ".example.com"
|
||||
*/
|
||||
domain?: string | undefined;
|
||||
}
|
||||
|
||||
function getCookieValue(name: string): string | null {
|
||||
@@ -46,7 +56,8 @@ export const lastLoginMethodClient = (
|
||||
*/
|
||||
clearLastUsedLoginMethod: (): void => {
|
||||
if (typeof document !== "undefined") {
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
const domainPart = config.domain ? ` domain=${config.domain};` : "";
|
||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;${domainPart}`;
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -703,6 +703,86 @@ describe("lastLoginMethod", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/9276
|
||||
*/
|
||||
it("should clear cross-subdomain cookies by including the domain attribute", async () => {
|
||||
const { client, testUser } = await getTestInstance(
|
||||
{
|
||||
baseURL: "https://auth.example.com",
|
||||
advanced: {
|
||||
crossSubDomainCookies: {
|
||||
enabled: true,
|
||||
domain: "example.com",
|
||||
},
|
||||
},
|
||||
plugins: [lastLoginMethod()],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [lastLoginMethodClient({ domain: "example.com" })],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Step 1: Sign in to get the cookie set with domain attribute
|
||||
let setCookieHeader = "";
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onResponse(context) {
|
||||
setCookieHeader = context.response.headers.get("set-cookie") || "";
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Verify the server sets the cookie with Domain=example.com
|
||||
expect(setCookieHeader).toContain(
|
||||
"better-auth.last_used_login_method=email",
|
||||
);
|
||||
expect(setCookieHeader).toContain("Domain=example.com");
|
||||
|
||||
// Step 2: Simulate clearing the cookie on the client side.
|
||||
// Mock document.cookie to capture the string the client writes.
|
||||
let writtenCookie = "";
|
||||
const originalDescriptor = Object.getOwnPropertyDescriptor(
|
||||
globalThis,
|
||||
"document",
|
||||
);
|
||||
|
||||
Object.defineProperty(globalThis, "document", {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(globalThis.document, "cookie", {
|
||||
get() {
|
||||
return "better-auth.last_used_login_method=email";
|
||||
},
|
||||
set(val: string) {
|
||||
writtenCookie = val;
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
client.clearLastUsedLoginMethod();
|
||||
|
||||
// Restore
|
||||
if (originalDescriptor) {
|
||||
Object.defineProperty(globalThis, "document", originalDescriptor);
|
||||
} else {
|
||||
globalThis.document = undefined as any;
|
||||
}
|
||||
|
||||
// The written cookie MUST include the domain to properly expire
|
||||
// a cross-subdomain cookie.
|
||||
expect(writtenCookie).toContain("domain=example.com");
|
||||
});
|
||||
|
||||
it("should handle multiple set-cookie headers correctly", async () => {
|
||||
// Create a custom plugin that sets an additional cookie to simulate multiple Set-Cookie headers
|
||||
const multiCookiePlugin = {
|
||||
|
||||
Generated
+9
-9
@@ -16977,14 +16977,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
jest-mock: 29.7.0
|
||||
|
||||
'@jest/fake-timers@29.7.0':
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@sinonjs/fake-timers': 10.3.0
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
@@ -17018,7 +17018,7 @@ snapshots:
|
||||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
'@types/yargs': 17.0.35
|
||||
chalk: 4.1.2
|
||||
|
||||
@@ -20244,7 +20244,7 @@ snapshots:
|
||||
|
||||
'@types/readable-stream@4.0.23':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
@@ -20256,7 +20256,7 @@ snapshots:
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
@@ -20289,7 +20289,7 @@ snapshots:
|
||||
|
||||
'@types/ws@8.18.1':
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
||||
@@ -23319,7 +23319,7 @@ snapshots:
|
||||
|
||||
happy-dom@20.8.9:
|
||||
dependencies:
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
'@types/whatwg-mimetype': 3.0.2
|
||||
'@types/ws': 8.18.1
|
||||
entities: 7.0.1
|
||||
@@ -23903,7 +23903,7 @@ snapshots:
|
||||
jest-mock@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
jest-util: 29.7.0
|
||||
|
||||
jest-regex-util@29.6.3: {}
|
||||
@@ -23911,7 +23911,7 @@ snapshots:
|
||||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 25.5.0
|
||||
'@types/node': 25.6.0
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
Reference in New Issue
Block a user