From eebd47a0e7103caae35e4ac46971e293d1579594 Mon Sep 17 00:00:00 2001 From: breken Date: Thu, 15 Oct 2026 06:15:38 -0700 Subject: [PATCH] fix(last-login-method): record Google One Tap sign-ins (#11393) Co-authored-by: arjunpkulkarni Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com> Co-authored-by: Gustavo Valverde --- .changeset/happy-one-tap-method.md | 7 +++ .../docs/plugins/last-login-method.mdx | 5 ++ .../src/plugins/last-login-method/index.ts | 3 ++ .../src/plugins/one-tap/one-tap.test.ts | 48 +++++++++++++++++++ 4 files changed, 63 insertions(+) create mode 100644 .changeset/happy-one-tap-method.md diff --git a/.changeset/happy-one-tap-method.md b/.changeset/happy-one-tap-method.md new file mode 100644 index 0000000000..d56923bb66 --- /dev/null +++ b/.changeset/happy-one-tap-method.md @@ -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. diff --git a/docs/content/docs/plugins/last-login-method.mdx b/docs/content/docs/plugins/last-login-method.mdx index ac8b4b7282..655ff96491 100644 --- a/docs/content/docs/plugins/last-login-method.mdx +++ b/docs/content/docs/plugins/last-login-method.mdx @@ -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 diff --git a/packages/better-auth/src/plugins/last-login-method/index.ts b/packages/better-auth/src/plugins/last-login-method/index.ts index 9e9281cade..fc42b743e6 100644 --- a/packages/better-auth/src/plugins/last-login-method/index.ts +++ b/packages/better-auth/src/plugins/last-login-method/index.ts @@ -82,6 +82,9 @@ export const lastLoginMethod = ( 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"; diff --git a/packages/better-auth/src/plugins/one-tap/one-tap.test.ts b/packages/better-auth/src/plugins/one-tap/one-tap.test.ts index f04b5ffcc6..016f9eb64c 100644 --- a/packages/better-auth/src/plugins/one-tap/one-tap.test.ts +++ b/packages/better-auth/src/plugins/one-tap/one-tap.test.ts @@ -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);