fix(GRO-545): switch OAuth state to cookie storage and add login error display

The OAuth callback was failing with "please_restart_the_process" because
Better-Auth's default DB-backed state (verification table) was unreliable —
the UAT hourly reset wipes all tables including verification records. Switch
to cookie-based state storage so the encrypted state survives in the browser
cookie across the redirect flow.

Also removes explicit redirectURI from socialProviders (Better-Auth derives
it from baseURL) and adds visible error feedback on the login page when
OAuth callbacks fail.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Paperclip
2026-04-11 18:01:59 +00:00
parent 1d76c63137
commit 085c8b9cfa
2 changed files with 18 additions and 5 deletions
+3 -4
View File
@@ -170,8 +170,6 @@ export async function initAuth(): Promise<void> {
const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET);
const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET);
const callbackBase = `${BETTER_AUTH_URL}/api/auth/callback`;
// Build Better-Auth instance using resolved config // Build Better-Auth instance using resolved config
authInstance = betterAuth({ authInstance = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -179,6 +177,9 @@ export async function initAuth(): Promise<void> {
}), }),
secret: BETTER_AUTH_SECRET, secret: BETTER_AUTH_SECRET,
baseURL: BETTER_AUTH_URL, baseURL: BETTER_AUTH_URL,
account: {
storeStateStrategy: "cookie" as const,
},
plugins: [ plugins: [
genericOAuth({ genericOAuth({
config: [ config: [
@@ -205,14 +206,12 @@ export async function initAuth(): Promise<void> {
google: { google: {
clientId: process.env.GOOGLE_CLIENT_ID!, clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectURI: `${callbackBase}/google`,
}, },
} : {}), } : {}),
...(hasGitHub ? { ...(hasGitHub ? {
github: { github: {
clientId: process.env.GITHUB_CLIENT_ID!, clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!, clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectURI: `${callbackBase}/github`,
}, },
} : {}), } : {}),
}, },
+15 -1
View File
@@ -23,17 +23,26 @@ import { useSession, signIn } from "./lib/auth-client.js";
function LoginPage() { function LoginPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [providers, setProviders] = useState<string[]>([]); const [providers, setProviders] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch("/api/auth/providers") fetch("/api/auth/providers")
.then((r) => r.json()) .then((r) => r.json())
.then((data) => setProviders(data.providers ?? [])) .then((data) => setProviders(data.providers ?? []))
.catch(() => setProviders([])); .catch(() => setProviders([]));
const params = new URLSearchParams(window.location.search);
const authError = params.get("error");
if (authError) setError(authError.replace(/_/g, " "));
}, []); }, []);
const handleSocialLogin = async (provider: string) => { const handleSocialLogin = async (provider: string) => {
setIsLoading(true); setIsLoading(true);
await signIn.social({ provider, callbackURL: window.location.origin }); setError(null);
const result = await signIn.social({ provider, callbackURL: window.location.origin });
if (result?.error) {
setError(result.error.message ?? "Sign-in failed");
setIsLoading(false);
}
}; };
const isGoogle = providers.includes("google"); const isGoogle = providers.includes("google");
@@ -65,6 +74,11 @@ function LoginPage() {
<p style={{ color: "#6b7280", marginBottom: "1.5rem", fontSize: 14 }}> <p style={{ color: "#6b7280", marginBottom: "1.5rem", fontSize: 14 }}>
Sign in to continue Sign in to continue
</p> </p>
{error && (
<div style={{ background: "#fef2f2", border: "1px solid #fecaca", borderRadius: 6, padding: "0.5rem 0.75rem", marginBottom: "1rem", color: "#991b1b", fontSize: 13 }}>
{error}
</div>
)}
{isGoogle && ( {isGoogle && (
<button <button
onClick={() => handleSocialLogin("google")} onClick={() => handleSocialLogin("google")}