fix(auth): avoid dropping session_token Set-Cookie on sign-in (#2718)

* fix credentials sign-in session cookie handling

* refactor(auth): rename originWith to withHostname

* refactor(auth): preserve localhost/127.0.0.1 sibling trust with LOCAL_ORIGINS

* update dependencies, update code style

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
Yeung
2026-02-24 17:08:32 +08:00
committed by GitHub
parent f73a5117a1
commit 8b3f3bcc35
4 changed files with 254 additions and 251 deletions
+19 -3
View File
@@ -3,7 +3,6 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { apiKey, type GenericOAuthConfig, genericOAuth, openAPI, twoFactor } from "better-auth/plugins";
import { username } from "better-auth/plugins/username";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { and, eq, or } from "drizzle-orm";
import { db } from "@/integrations/drizzle/client";
import { env } from "@/utils/env";
@@ -20,6 +19,24 @@ function isCustomOAuthProviderEnabled() {
return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
}
function getTrustedOrigins(): string[] {
const appUrl = new URL(env.APP_URL);
const trustedOrigins = new Set<string>([appUrl.origin.replace(/\/$/, "")]);
const LOCAL_ORIGINS = ["localhost", "127.0.0.1"];
if (LOCAL_ORIGINS.includes(appUrl.hostname)) {
for (const hostname of LOCAL_ORIGINS) {
if (hostname !== appUrl.hostname) {
const altUrl = new URL(env.APP_URL);
altUrl.hostname = hostname;
trustedOrigins.add(altUrl.origin.replace(/\/$/, ""));
}
}
}
return Array.from(trustedOrigins);
}
const getAuthConfig = () => {
const authConfigs: GenericOAuthConfig[] = [];
@@ -69,7 +86,7 @@ const getAuthConfig = () => {
database: drizzleAdapter(db, { schema, provider: "pg" }),
telemetry: { enabled: false },
trustedOrigins: [env.APP_URL],
trustedOrigins: getTrustedOrigins(),
advanced: {
database: { generateId },
useSecureCookies: env.APP_URL.startsWith("https://"),
@@ -229,7 +246,6 @@ const getAuthConfig = () => {
}),
twoFactor({ issuer: "Reactive Resume" }),
genericOAuth({ config: authConfigs }),
tanstackStartCookies(),
],
});
};
+28 -29
View File
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { createFileRoute, Link, redirect, useNavigate, useRouter } from "@tanstack/react-router";
import type { BetterFetchOption } from "better-auth/client";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { useToggle } from "usehooks-ts";
@@ -46,37 +45,37 @@ function RouteComponent() {
const onSubmit = async (data: FormValues) => {
const toastId = toast.loading(t`Signing in...`);
const fetchOptions: BetterFetchOption = {
onSuccess: (context) => {
// Check if 2FA is required
if (context.data && "twoFactorRedirect" in context.data && context.data.twoFactorRedirect) {
toast.dismiss(toastId);
navigate({ to: "/auth/verify-2fa", replace: true });
return;
}
try {
const isEmail = data.identifier.includes("@");
// Normal login success
router.invalidate();
const result = isEmail
? await authClient.signIn.email({ email: data.identifier, password: data.password })
: await authClient.signIn.username({ username: data.identifier, password: data.password });
if (result.error) {
toast.error(result.error.message, { id: toastId });
return;
}
const requiresTwoFactor =
result.data &&
typeof result.data === "object" &&
"twoFactorRedirect" in result.data &&
result.data.twoFactorRedirect;
// Credential check passed, but the account still requires a 2FA verification step.
if (requiresTwoFactor) {
toast.dismiss(toastId);
navigate({ to: "/dashboard", replace: true });
},
onError: ({ error }) => {
toast.error(error.message, { id: toastId });
},
};
navigate({ to: "/auth/verify-2fa", replace: true });
return;
}
if (data.identifier.includes("@")) {
await authClient.signIn.email({
email: data.identifier,
password: data.password,
fetchOptions,
});
} else {
await authClient.signIn.username({
username: data.identifier,
password: data.password,
fetchOptions,
});
// Refresh route context so protected routes can read the newly established session.
await router.invalidate();
toast.dismiss(toastId);
navigate({ to: "/dashboard", replace: true });
} catch {
toast.error(t`Failed to sign in. Please try again.`, { id: toastId });
}
};