[GH-ISSUE #3004] Twitter provider incorrectly sets email verification status #18062

Closed
opened 2026-04-15 16:24:59 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @Xirynx on GitHub (Jun 13, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/3004

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Set up better auth with twitter social sign in
  2. Sign in with a twitter account that does not have any records in the db yet

Current vs. Expected behavior

By doing this, a new user is created, however, the emailVerification field is always false, due to the following implementation:

async getUserInfo(token) {
  if (options.getUserInfo) {
    return options.getUserInfo(token);
  }
  const { data: profile, error: profileError } =
    await betterFetch<TwitterProfile>(
      "https://api.x.com/2/users/me?user.fields=profile_image_url",
      {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token.accessToken}`,
        },
      }
    );

  if (profileError) {
    return null;
  }

  const { data: emailData, error: emailError } = await betterFetch<{
    data: { confirmed_email: string };
  }>("https://api.x.com/2/users/me?user.fields=confirmed_email", {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token.accessToken}`,
    },
  });
  if (!emailError && emailData?.data?.confirmed_email) {
    profile.data.email = emailData.data.confirmed_email;
  }
  const userMap = await options.mapProfileToUser?.(profile);
  return {
    user: {
      id: profile.data.id,
      name: profile.data.name,
      email: profile.data.email || profile.data.username || null,
      image: profile.data.profile_image_url,
      emailVerified: profile.data.verified || false,
      ...userMap,
    },
    data: profile,
  };
},

The user.emailVerified property in the returned object uses profile.data.verified which is incorrect for two reasons:

  1. I suspect that verified refers to whether a twitter account has a checkmark, rather than email verification.
  2. verified is never returned from the Twitter API request due to the verified field not being requested.

The correct way to check this would be by checking if the confirmed_email field exists (to the best of my knowledge).

What version of Better Auth are you using?

1.2.9

Provide environment information

- OS: Arch Linux
- Browser: Brave

Which area(s) are affected? (Select all that apply)

Package

Auth config (if applicable)

export const auth = betterAuth({
    emailAndPassword: {
        enabled: true,
        requireEmailVerification: true,
        sendResetPassword: async ({ user, url }) => {
            await sendEmail({
                to: user.email,
                subject: 'Reset your password',
                text: `Click the link to reset your password: ${url}`,
            });
        },
    },
    emailVerification: {
        sendOnSignUp: true,
        autoSignInAfterVerification: true,
        sendVerificationEmail: async ({ user, url }) => {
            await sendEmail({
                to: user.email,
                subject: 'Verify your email address',
                text: `Click the link to verify your email: ${url}`,
            });
        },
    },
    socialProviders: {
        google: {
            clientId: env.GOOGLE_CLIENT_ID,
            clientSecret: env.GOOGLE_CLIENT_SECRET,
        },
        twitter: {
            clientId: env.TWITTER_CLIENT_ID,
            clientSecret: env.TWITTER_CLIENT_SECRET,
        },
    },
    advanced: {
        crossSubDomainCookies: {
            enabled: env.NODE_ENV === 'production',
            domain: env.COOKIE_DOMAIN, // Domain with a leading period
        },
        defaultCookieAttributes: {
            secure: true,
            httpOnly: true,
            sameSite: 'none',  // Allows CORS-based cookie sharing across subdomains
            partitioned: true, // New browser standards will mandate this for foreign cookies
        },
    },
    database: drizzleAdapter(database, {
        provider: 'pg',
    }),
    trustedOrigins: env.ALLOWED_ORIGINS,
    basePath: '/auth',
    plugins: [
        admin(),
        openAPI(),
    ],
});

Additional context

No response

Originally created by @Xirynx on GitHub (Jun 13, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/3004 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Set up better auth with twitter social sign in 2. Sign in with a twitter account that does not have any records in the db yet ### Current vs. Expected behavior By doing this, a new user is created, however, the `emailVerification` field is always false, due to the following implementation: ```ts async getUserInfo(token) { if (options.getUserInfo) { return options.getUserInfo(token); } const { data: profile, error: profileError } = await betterFetch<TwitterProfile>( "https://api.x.com/2/users/me?user.fields=profile_image_url", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}`, }, } ); if (profileError) { return null; } const { data: emailData, error: emailError } = await betterFetch<{ data: { confirmed_email: string }; }>("https://api.x.com/2/users/me?user.fields=confirmed_email", { method: "GET", headers: { Authorization: `Bearer ${token.accessToken}`, }, }); if (!emailError && emailData?.data?.confirmed_email) { profile.data.email = emailData.data.confirmed_email; } const userMap = await options.mapProfileToUser?.(profile); return { user: { id: profile.data.id, name: profile.data.name, email: profile.data.email || profile.data.username || null, image: profile.data.profile_image_url, emailVerified: profile.data.verified || false, ...userMap, }, data: profile, }; }, ``` The `user.emailVerified` property in the returned object uses `profile.data.verified` which is incorrect for two reasons: 1. I suspect that `verified` refers to whether a twitter account has a checkmark, rather than email verification. 2. `verified` is never returned from the Twitter API request due to the verified field not being requested. The correct way to check this would be by checking if the `confirmed_email` field exists (to the best of my knowledge). ### What version of Better Auth are you using? 1.2.9 ### Provide environment information ```bash - OS: Arch Linux - Browser: Brave ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript export const auth = betterAuth({ emailAndPassword: { enabled: true, requireEmailVerification: true, sendResetPassword: async ({ user, url }) => { await sendEmail({ to: user.email, subject: 'Reset your password', text: `Click the link to reset your password: ${url}`, }); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { await sendEmail({ to: user.email, subject: 'Verify your email address', text: `Click the link to verify your email: ${url}`, }); }, }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }, twitter: { clientId: env.TWITTER_CLIENT_ID, clientSecret: env.TWITTER_CLIENT_SECRET, }, }, advanced: { crossSubDomainCookies: { enabled: env.NODE_ENV === 'production', domain: env.COOKIE_DOMAIN, // Domain with a leading period }, defaultCookieAttributes: { secure: true, httpOnly: true, sameSite: 'none', // Allows CORS-based cookie sharing across subdomains partitioned: true, // New browser standards will mandate this for foreign cookies }, }, database: drizzleAdapter(database, { provider: 'pg', }), trustedOrigins: env.ALLOWED_ORIGINS, basePath: '/auth', plugins: [ admin(), openAPI(), ], }); ``` ### Additional context _No response_
GiteaMirror added the lockedbug labels 2026-04-15 16:24:59 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#18062