Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.

Commit ed07df0

Browse files
authored
Merge pull request #16 from initstring/codex/fix-oauth-login-flow-behavior
Fix Google OAuth linking to existing accounts
2 parents 11e30f8 + f15531b commit ed07df0

2 files changed

Lines changed: 92 additions & 4 deletions

File tree

src/server/auth/config.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ export const authConfig = {
197197
GoogleProvider({
198198
clientId: process.env.GOOGLE_CLIENT_ID,
199199
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
200+
// We trust the locally provisioned accounts and block unknown e-mails in the
201+
// sign-in callback, so allow Auth.js to link Google users directly by e-mail.
202+
allowDangerousEmailAccountLinking: true,
200203
}),
201204
]
202205
: []),
@@ -241,8 +244,32 @@ export const authConfig = {
241244
const emailAddr = (user as { email?: string | null } | undefined)?.email?.toLowerCase();
242245
if (!emailAddr) return false;
243246
try {
244-
const existing = await db.user.findUnique({ where: { email: emailAddr } });
245-
return Boolean(existing);
247+
const existing = await db.user.findUnique({
248+
where: { email: emailAddr },
249+
select: { id: true, emailVerified: true },
250+
});
251+
if (!existing) return false;
252+
253+
if (!existing.emailVerified) {
254+
try {
255+
await db.user.update({
256+
where: { id: existing.id },
257+
data: { emailVerified: new Date() },
258+
});
259+
} catch (updateError) {
260+
logger.warn(
261+
{
262+
event: "auth.oauth_email_verified_update_failed",
263+
provider,
264+
userId: existing.id,
265+
error: updateError,
266+
},
267+
"Failed to mark OAuth user as verified",
268+
);
269+
}
270+
}
271+
272+
return true;
246273
} catch (error) {
247274
logger.warn(
248275
{ event: "auth.oauth_validation_error", provider, error },

src/test/auth-signin-callback.test.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import { describe, it, expect, vi } from "vitest";
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
22
import type { AdapterUser } from "next-auth/adapters";
33

44
// Mock server-only barrier and db import so config can be imported in Vitest
55
vi.mock("server-only", () => ({}));
6-
vi.mock("@/server/db", () => ({ db: { user: { findUnique: vi.fn() } } }));
6+
vi.mock("@/server/db", () => ({ db: { user: { findUnique: vi.fn(), update: vi.fn() } } }));
77

88
const { db } = await import("@/server/db");
99
const mockDb = vi.mocked(db, true);
1010

11+
beforeEach(() => {
12+
mockDb.user.findUnique.mockReset();
13+
mockDb.user.update.mockReset();
14+
});
15+
1116
describe("NextAuth signIn callback", () => {
1217
it("allows calls without account context", async () => {
1318
const { authConfig } = await import("@/server/auth/config");
@@ -39,4 +44,60 @@ describe("NextAuth signIn callback", () => {
3944
});
4045
expect(res).toBe(true);
4146
});
47+
48+
it("allows Google sign-in for existing user and marks email verified", async () => {
49+
mockDb.user.findUnique.mockResolvedValue({ id: "u1", emailVerified: null });
50+
mockDb.user.update.mockResolvedValue({ id: "u1" } as never);
51+
52+
const { authConfig } = await import("@/server/auth/config");
53+
const signInCb = authConfig.callbacks?.signIn;
54+
if (!signInCb) throw new Error("signIn callback missing");
55+
56+
const res = await signInCb({
57+
account: { provider: "google" } as any,
58+
user: { email: "User@Test.com" } as AdapterUser,
59+
});
60+
61+
expect(res).toBe(true);
62+
expect(mockDb.user.findUnique).toHaveBeenCalledWith({
63+
where: { email: "user@test.com" },
64+
select: { id: true, emailVerified: true },
65+
});
66+
expect(mockDb.user.update).toHaveBeenCalledTimes(1);
67+
const updateArg = mockDb.user.update.mock.calls[0]?.[0];
68+
expect(updateArg).toMatchObject({ where: { id: "u1" } });
69+
expect(updateArg?.data?.emailVerified).toBeInstanceOf(Date);
70+
});
71+
72+
it("skips verification update when Google user already verified", async () => {
73+
mockDb.user.findUnique.mockResolvedValue({ id: "u2", emailVerified: new Date("2024-01-01T00:00:00.000Z") });
74+
75+
const { authConfig } = await import("@/server/auth/config");
76+
const signInCb = authConfig.callbacks?.signIn;
77+
if (!signInCb) throw new Error("signIn callback missing");
78+
79+
const res = await signInCb({
80+
account: { provider: "google" } as any,
81+
user: { email: "verified@test.com" } as AdapterUser,
82+
});
83+
84+
expect(res).toBe(true);
85+
expect(mockDb.user.update).not.toHaveBeenCalled();
86+
});
87+
88+
it("denies Google sign-in when user does not exist", async () => {
89+
mockDb.user.findUnique.mockResolvedValue(null);
90+
91+
const { authConfig } = await import("@/server/auth/config");
92+
const signInCb = authConfig.callbacks?.signIn;
93+
if (!signInCb) throw new Error("signIn callback missing");
94+
95+
const res = await signInCb({
96+
account: { provider: "google" } as any,
97+
user: { email: "missing@test.com" } as AdapterUser,
98+
});
99+
100+
expect(res).toBe(false);
101+
expect(mockDb.user.update).not.toHaveBeenCalled();
102+
});
42103
});

0 commit comments

Comments
 (0)