fix(last-login-method): record Google One Tap sign-ins (#11393)

Co-authored-by: arjunpkulkarni <arjunpkulkarni@gmail.com>
Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
Co-authored-by: Gustavo Valverde <g.valverde02@gmail.com>
This commit is contained in:
breken
2026-10-15 14:15:38 +01:00
committed by GitHub
co-authored by arjunpkulkarni breken-ai Gustavo Valverde
parent eaecb7229e
commit eebd47a0e7
4 changed files with 63 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"better-auth": patch
---
If you use the `lastLoginMethod` plugin with `oneTap`, signing in through Google One Tap now records `"google"`, the same value as signing in with the Google button. Before, a One Tap sign-in recorded nothing: the `better-auth.last_used_login_method` cookie kept its previous value or stayed unset, and so did `user.lastLoginMethod` with `storeInDatabase`.
If your `customResolveMethod` includes a `/one-tap/callback` → `"google"` workaround, remove that mapping; keep the resolver if it handles other routes.
@@ -379,6 +379,11 @@ The plugin automatically detects the method from these endpoints:
* `/callback/:id` - OAuth callback with provider ID
* `/sign-in/email` - Email sign in
* `/sign-up/email` - Email sign up
* `/one-tap/callback` - Google One Tap sign in, recorded as `"google"`
* `/siwe/verify` - Sign-In with Ethereum, recorded as `"siwe"`
* `/passkey/verify-authentication` - Passkey sign in, recorded as `"passkey"`
* `/magic-link/verify` - Magic link sign in, recorded as `"magic-link"`
* `/sign-in/email-otp` - Email OTP sign in, recorded as `"email-otp"`
## Cross-Domain Support
@@ -82,6 +82,9 @@ export const lastLoginMethod = <O extends LastLoginMethodOptions>(
if (path.startsWith("/callback/")) {
return ctx.params?.id || path.split("/").pop();
}
if (path === "/one-tap/callback") {
return "google";
}
// Check for email sign-in/sign-up
if (path === "/sign-in/email" || path === "/sign-up/email") {
return "email";
@@ -6,6 +6,7 @@ import { jwtVerify } from "jose";
import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
import { createAuthClient } from "../../client";
import { getTestInstance } from "../../test-utils/test-instance";
import { lastLoginMethod } from "../last-login-method";
import { oneTapClient } from "./client";
import { oneTap } from "./index";
@@ -62,6 +63,53 @@ afterEach(() => {
vi.mocked(jwtVerify).mockClear();
});
/**
* @see https://github.com/better-auth/better-auth/issues/10995
*/
describe("one-tap last login method", () => {
it("records Google as the last login method after a one-tap callback", async () => {
const { auth } = await getTestInstance({
plugins: [oneTap({ clientId: "test-google-client" }), lastLoginMethod()],
});
const response = await auth.handler(
new Request("http://localhost:3000/api/auth/one-tap/callback", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idToken: "stub-id-token" }),
}),
);
expect(response.status).toBe(200);
expect(response.headers.getSetCookie()).toContainEqual(
expect.stringContaining("better-auth.last_used_login_method=google"),
);
});
it("persists Google as the last login method after a one-tap callback", async () => {
const { auth } = await getTestInstance({
plugins: [
oneTap({ clientId: "test-google-client" }),
lastLoginMethod({ storeInDatabase: true }),
],
});
const response = await auth.handler(
new Request("http://localhost:3000/api/auth/one-tap/callback", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idToken: "stub-id-token" }),
}),
);
expect(response.status).toBe(200);
const context = await auth.$context;
const user = await context.adapter.findOne<{ lastLoginMethod: string }>({
model: "user",
where: [{ field: "email", value: defaultVerifiedPayload.email }],
});
expect(user?.lastLoginMethod).toBe("google");
});
});
describe("one-tap implicit linking gate", async () => {
afterEach(() => {
Object.assign(verifiedPayload, defaultVerifiedPayload);