oAuthProxy not casuing a bad state on social provider #2200

Closed
opened 2026-03-13 09:33:58 -05:00 by GiteaMirror · 5 comments
Owner

Originally created by @ryunosuke21 on GitHub (Oct 24, 2025).

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Configure Better Auth
  2. Add oAuth Proxy plugin
  3. Upload the application to any platform (I tried with Azure, but in discord there's a discussion about the same issue on Cloudflare)
  4. Add productionURL to oAuthProxy
  5. Start dev environment
  6. Test social sign in

This issue is happening with google social provider, in a self hosted Next JS on a VPS.

Current vs. Expected behavior

Following the steps of the oAuth Proxy Plugin I'd expect better-auth to skip checking for cookie state and proxy the request from the production url to the client url.

In actuallity we get a please_restart_the_process error from the Social Provider due to the lack of state.

What version of Better Auth are you using?

1.3.30

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025",
    "release": "6.6.87.2-microsoft-standard-WSL2",
    "cpuCount": 12,
    "cpuModel": "AMD Ryzen 5 5600X 6-Core Processor",
    "totalMemory": "15.58 GB",
    "freeMemory": "11.51 GB"
  },
  "node": {
    "version": "v24.6.0",
    "env": "development"
  },
  "packageManager": {
    "name": "pnpm",
    "version": "10.19.0"
  },
  "frameworks": [
    {
      "name": "next",
      "version": "^15.5.4"
    },
    {
      "name": "react",
      "version": "^19.1.1"
    }
  ],
  "databases": [
    {
      "name": "postgres",
      "version": "^3.4.7"
    },
    {
      "name": "drizzle",
      "version": "^0.44.5"
    },
    {
      "name": "mongodb",
      "version": "^6.20.0"
    }
  ],
  "betterAuth": {
    "version": "^1.3.30",
    "config": null
  }
}

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

Package

Auth config (if applicable)

export const auth = betterAuth({
  appName: APP_NAME,
  trustedOrigins: [
    env.BASE_URL,
    env.PRODUCTION_URL,
    "http://localhost:3000",
  ],

  /* SECTION: Database */
  database: drizzleAdapter(db, {
    provider: "pg",
    schema,
    usePlural: true,
    camelCase: false,
    debugLogs: env.NODE_ENV === "development",
    transaction: true,
  }),
  /* SECTION: Authentication */
  secret: env.BETTER_AUTH_SECRET,
  emailAndPassword: {
    enabled: true,
    autoSignIn: false,
    maxPasswordLength: MAX_PASSWORD_LENGTH,
    minPasswordLength: MIN_PASSWORD_LENGTH,

    password: {
      hash: async (password) => {
        logger.debug("Hashing password", password);
        const hashedPassword = await hashBuffer.digest(password);
        logger.debug("Hashed password", hashedPassword);
        return hashedPassword;
      },
      verify: async ({ password, hash }) => {
        logger.debug("Verifying password", { password, hash });
        const hashedPassword = await hashBuffer.digest(password);
        logger.debug("Hashed password", hashedPassword);
        return hashedPassword === hash;
      },
    },
    requireEmailVerification: true,
    resetPasswordTokenExpiresIn: SHORT_LIVED_TOKEN,
    revokeSessionsOnPasswordReset: true,

    onPasswordReset: async ({ user }) => {
      try {
        logger.info("Password reset successfully", { user });

        const emailPasswordReset = await renderPasswordResetEmail({
          baseUrl: env.BASE_URL,
          name: user.name,
          loginLink: env.BASE_URL,
        });

        await emailSender({
          toEmail: user.email,
          subject: "Password Reset Successful",
          html: emailPasswordReset,
        });
      } catch (error) {
        logger.error("Error in onPasswordReset hook", { error, user });
        throw error;
      }
    },
    sendResetPassword: async ({ user, url }) => {
      try {
        const resetUrl = new URL(url);

        let callbackUrl = resetUrl.searchParams.get("callbackURL");

        if (!callbackUrl || callbackUrl.trim() === "") {
          url = url + env.BASE_URL;
        }

        logger.info("Sending password reset email", { user, url });

        const emailPasswordRequest = await renderPasswordResetRequestEmail({
          baseUrl: env.BASE_URL,
          name: user.name,
          resetLink: url,
          period: SHORT_LIVED_TOKEN,
        });

        await emailSender({
          toEmail: user.email,
          subject: "Password Reset Request",
          html: emailPasswordRequest,
          text: `Hi ${user.name || "User"}. You can reset your password by clicking the link below: ${url}.`,
        });
      } catch (error) {
        logger.error("Error sending reset password email", { error, user });
        throw error;
      }
    },
  },
  socialProviders: {
    google: {
      clientId: env.GOOGLE_CLIENT_ID,
      clientSecret: env.GOOGLE_CLIENT_SECRET,
      prompt: "select_account",
      redirectURI: `${env.PRODUCTION_URL}/api/auth/callback/google`,
    },
    /* TODO Add Apple provider */
  },
  emailVerification: {
    onEmailVerification: async (user) => {
      try {
        // Add comprehensive safety checks for the user object
        if (!user) {
          logger.error("onEmailVerification: user object is null or undefined");
          return;
        }

        if (!user.email) {
          logger.error("onEmailVerification: user.email is missing", { user });
          return;
        }

        // Ensure user.name exists, fallback to user.email username or "User" if name is not available
        const userName = user.name || user.email?.split("@")[0] || "User";

        logger.info("Processing email verification", {
          userId: user.id,
          email: user.email,
          name: userName,
          userObject: user,
        });

        const email = await renderAccountVerifiedEmail({
          baseUrl: env.BASE_URL,
          name: userName,
        });

        await emailSender({
          toEmail: user.email,
          subject: "Account Verified",
          html: email,
          text: `Hi ${userName}. Your account has been successfully verified. You can now log in and start using our services.`,
        });

        logger.info("Email verification successful", {
          userId: user.id,
          email: user.email,
        });
      } catch (error) {
        logger.error("Error in email verification callback", {
          error,
          user: user || "user is null/undefined",
        });
        // Don't re-throw the error to prevent blocking the verification process
      }
    },
    sendVerificationEmail: async ({ url, user }) => {
      try {
        if (!url.includes("verification=true")) {
          if (url.includes("?")) {
            url = url + "&verification=true";
          } else {
            url = url + "?verification=true";
          }
        }

        // Ensure user.name exists, fallback to user.email username or "User" if name is not available
        const userName = user.name || user.email?.split("@")[0] || "User";

        logger.info("Sending verification email", {
          userId: user.id,
          email: user.email,
          name: userName,
          url,
        });

        const email = await renderAccountVerificationEmail({
          baseUrl: env.BASE_URL,
          name: userName,
          verificationLink: url,
        });

        await emailSender({
          toEmail: user.email,
          subject: "Account Verification",
          html: email,
          text: `Hi ${userName}. Please click the link below to verify your account: ${url}`,
        });

        logger.info("Verification email sent successfully", {
          userId: user.id,
          email: user.email,
        });
      } catch (error) {
        logger.error("Error sending verification email", { error, user });
        throw error;
      }
    },
    async afterEmailVerification(user, request) {
      logger.info("Email verified successfully", { user });
    },
    sendOnSignUp: true,
  },
  verification: {
    disableCleanup: env.NODE_ENV === "development",
    verificationTokenExpiresIn: 24 * 60 * 60, // 24 hours in seconds
  },

  /* SECTION: User Settings */
  user: {
    changeEmail: {
      enabled: true,
      sendChangeEmailVerification: async ({ user }) => {
        logger.debug("Sending change email verification", { user });
      },
    },
    deleteUser: {
      enabled: true,
      deleteTokenExpiresIn: SHORT_LIVED_TOKEN,
      beforeDelete: async (user) => {
        logger.info("User deletion started", { user });
      },
      afterDelete: async (user) => {
        logger.info("User deletion completed", { user });
      },
      sendDeleteAccountVerification: async ({ user }) => {
        logger.debug("Sending delete account verification", { user });
      },
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: true,
      allowUnlinkingAll: false,
      trustedProviders: ["google"],
    },
    encryptOAuthTokens: env.NODE_ENV === "development" ? false : true,
    updateAccountOnSignIn: true,
  },
  session: {
    freshAge: FRESH_TOKEN,
    expiresIn: undefined,
    cookieCache: {
      enabled: true,
      maxAge: SHORT_LIVED_TOKEN,
    },
    storeSessionInDatabase: true,
  },

  /* SECTION: Plugins */
  plugins: [
    username({
      displayUsernameNormalization: (username: string) => {
        return username
          .trim()
          .replace(/\s+/g, "_")
          .replace(/[^a-zA-Z0-9_-]/g, "");
      },
      usernameNormalization: (username: string) => {
        return username
          .trim()
          .replace(/\s+/g, "_")
          .replace(/[^a-zA-Z0-9_-]/g, "");
      },
      displayUsernameValidator: (username: string) => {
        return USERNAME_REGEX.test(username);
      },
      usernameValidator: (username: string) => {
        return USERNAME_REGEX.test(username);
      },
      maxUsernameLength: MAX_USERNAME_LENGTH,
      minUsernameLength: MIN_USERNAME_LENGTH,
      validationOrder: {
        username: "pre-normalization",
        displayUsername: "pre-normalization",
      },
    }),
    phoneNumber({
      sendOTP: async ({ phoneNumber }) => {
        logger.debug("Sending OTP", { phoneNumber });
      },
      requireVerification: false,
    }),
    admin({
      ac: adminAc,
      roles: {
        idp_super_admin,
        idp_system_admin,
        idp_security_admin,
        idp_user_manager,
        idp_developer,
        idp_support_agent,
        idp_auditor,
        idp_user,
      },
      adminRoles,
    }),
    organization({
      ac: tempOrgAc,
      dynamicAccessControl: {
        enabled: true,
      },
      creatorRole: "owner",
      invitationLimit: ({}) => {
        return 100;
      },
      allowUserToCreateOrganization: async ({ emailVerified }) => {
        return emailVerified;
      },
      cancelPendingInvitationsOnReInvite: true,
      disableOrganizationDeletion: false,
      invitationExpiresIn: LONG_LIVED_TOKEN,
      teams: {
        enabled: true,
        allowRemovingAllTeams: false,
        defaultTeam: {
          enabled: false,
        },
      },
      membershipLimit: 1000,
      organizationLimit: 5,
      requireEmailVerificationOnInvitation: true,
      sendInvitationEmail: async ({ email, inviter, organization }) => {
        logger.debug("Sending invitation email", { email });

        const emailInvite = await renderInviteEmail({
          baseUrl: env.BASE_URL,
          inviteLink: `${env.BASE_URL}/`,
          inviterEmail: inviter.user.email,
          inviterName: inviter.user.name,
          teamName: organization.name,
          name: email,
        });

        await emailSender({
          toEmail: email,
          subject: "Invitation to join organization",
          html: emailInvite,
        });
      },
    }),
    apiKey({
      defaultKeyLength: API_KEY_LENGTH,
      maximumNameLength: API_KEY_MAX_NAME_LENGTH,
      minimumNameLength: API_KEY_MIN_NAME_LENGTH,
      defaultPrefix: "GXS_API-",
      enableMetadata: true,
      disableKeyHashing: env.NODE_ENV === "development",
      requireName: true,
      startingCharactersConfig: {
        charactersLength: 12,
        shouldStore: true,
      },
    }),
    twoFactor({
      issuer: APP_SLUG,
      backupCodeOptions: {
        amount: BACKUP_CODES_AMMOUNT,
        length: BACKUP_CODES_LENGTH,
        storeBackupCodes:
          env.NODE_ENV === "development" ? "plain" : "encrypted",
      },
      otpOptions: {
        allowedAttempts: MAX_OTP_ATTEMPTS,
        digits: OTP_LENGTH,
        period: SHORT_LIVED_TOKEN,
        storeOTP: env.NODE_ENV === "development" ? "plain" : "hashed",
        sendOTP: async ({ user, otp }) => {
          try {
            const otpEmail = await renderOneTimePasswordEmail({
              baseUrl: env.BASE_URL,
              name: user.name,
              otp,
              period: SHORT_LIVED_TOKEN,
            });

            await emailSender({
              toEmail: user.email,
              toName: user.name,
              html: otpEmail,
              subject: "Your One-Time Password (OTP)",
            });
          } catch (error) {
            logger.error("Error Sending OTP", error);
            throw error;
          }
        },
      },

      totpOptions: {
        digits: OTP_LENGTH,
      },
      skipVerificationOnEnable: false,
    }),
    passkey({
      rpID: APP_SLUG,
      rpName: APP_NAME,
      origin: env.BASE_URL,
    }),
    lastLoginMethod({
      storeInDatabase: true,
      customResolveMethod: (ctx) => {
        if (ctx.path === "/oauth2/authorize") {
          return "oauth2";
        }
        return null;
      },
    }),
    bearer(),
    openAPI(),
    oAuthProxy({
      currentURL: env.BASE_URL,
      productionURL: env.PRODUCTION_URL,
    }),
    jwt({
      jwks: {
        keyPairConfig: {
          alg: "RS256",
        },
      },
    }),
  ],

  /* SECTION: Advanced */
  logger: {
    level: env.LOG_LEVEL,
    disableColors: false,
    disabled: false,
    log: (level, message, meta) => {
      const cleanMeta =
        meta && typeof meta === "object" && meta !== null
          ? Object.fromEntries(
              Object.entries(meta).filter(([k]) => k !== "ctx"),
            )
          : meta;
      if (level === "error") {
        logger.error(
          message,
          env.LOG_LEVEL === "debug" ? cleanMeta : undefined,
        );
      } else if (level === "warn") {
        logger.warn(message, env.LOG_LEVEL === "debug" ? cleanMeta : undefined);
      } else if (level === "info") {
        logger.info(message, env.LOG_LEVEL === "debug" ? cleanMeta : undefined);
      } else if (level === "debug") {
        logger.debug(message, cleanMeta);
      } else {
        logger.info(message, {
          level,
          internalMessage: "A log level that is not handled was created",
          ...cleanMeta,
        });
      }
    },
  },
  advanced: {
    ipAddress: {
      disableIpTracking: false,
    },
    database: {
      defaultFindManyLimit: 100,
    },
  },
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          logger.debug("User to be created", { user });
        },
        after: async (user) => {
          logger.debug("User created", { user });
        },
      },
    },
    verification: {
      create: {
        before: async (data) => {
          logger.debug("Verification to be created", { data });
        },
      },
    },
  },
  onAPIError: {
    errorURL: "/error",
    onError: (error) => {
      if (error instanceof Error && error.message) {
        logger.error(error.message, { error });
      }
      logger.error("An API error occurred", { error });
    },
    throw: false,
  },
  rateLimit: {
    enabled: env.NODE_ENV === "development" ? false : true,
    storage: "secondary-storage",
    window: 60 * 1, // 1 minute
    max: 25,
  },
});

Additional context

This discord issue details what could be happening.

Tldr:

Image
Originally created by @ryunosuke21 on GitHub (Oct 24, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Configure Better Auth 2. Add oAuth Proxy plugin 3. Upload the application to any platform (I tried with Azure, but in discord there's a discussion about the same issue on Cloudflare) 4. Add `productionURL` to oAuthProxy 5. Start dev environment 6. Test social sign in This issue is happening with google social provider, in a self hosted Next JS on a VPS. ### Current vs. Expected behavior Following the steps of the [oAuth Proxy Plugin](https://www.better-auth.com/docs/plugins/oauth-proxy) I'd expect better-auth to skip checking for cookie state and proxy the request from the production url to the client url. In actuallity we get a `please_restart_the_process` error from the Social Provider due to the lack of state. ### What version of Better Auth are you using? 1.3.30 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", "release": "6.6.87.2-microsoft-standard-WSL2", "cpuCount": 12, "cpuModel": "AMD Ryzen 5 5600X 6-Core Processor", "totalMemory": "15.58 GB", "freeMemory": "11.51 GB" }, "node": { "version": "v24.6.0", "env": "development" }, "packageManager": { "name": "pnpm", "version": "10.19.0" }, "frameworks": [ { "name": "next", "version": "^15.5.4" }, { "name": "react", "version": "^19.1.1" } ], "databases": [ { "name": "postgres", "version": "^3.4.7" }, { "name": "drizzle", "version": "^0.44.5" }, { "name": "mongodb", "version": "^6.20.0" } ], "betterAuth": { "version": "^1.3.30", "config": null } } ``` ### Which area(s) are affected? (Select all that apply) Package ### Auth config (if applicable) ```typescript export const auth = betterAuth({ appName: APP_NAME, trustedOrigins: [ env.BASE_URL, env.PRODUCTION_URL, "http://localhost:3000", ], /* SECTION: Database */ database: drizzleAdapter(db, { provider: "pg", schema, usePlural: true, camelCase: false, debugLogs: env.NODE_ENV === "development", transaction: true, }), /* SECTION: Authentication */ secret: env.BETTER_AUTH_SECRET, emailAndPassword: { enabled: true, autoSignIn: false, maxPasswordLength: MAX_PASSWORD_LENGTH, minPasswordLength: MIN_PASSWORD_LENGTH, password: { hash: async (password) => { logger.debug("Hashing password", password); const hashedPassword = await hashBuffer.digest(password); logger.debug("Hashed password", hashedPassword); return hashedPassword; }, verify: async ({ password, hash }) => { logger.debug("Verifying password", { password, hash }); const hashedPassword = await hashBuffer.digest(password); logger.debug("Hashed password", hashedPassword); return hashedPassword === hash; }, }, requireEmailVerification: true, resetPasswordTokenExpiresIn: SHORT_LIVED_TOKEN, revokeSessionsOnPasswordReset: true, onPasswordReset: async ({ user }) => { try { logger.info("Password reset successfully", { user }); const emailPasswordReset = await renderPasswordResetEmail({ baseUrl: env.BASE_URL, name: user.name, loginLink: env.BASE_URL, }); await emailSender({ toEmail: user.email, subject: "Password Reset Successful", html: emailPasswordReset, }); } catch (error) { logger.error("Error in onPasswordReset hook", { error, user }); throw error; } }, sendResetPassword: async ({ user, url }) => { try { const resetUrl = new URL(url); let callbackUrl = resetUrl.searchParams.get("callbackURL"); if (!callbackUrl || callbackUrl.trim() === "") { url = url + env.BASE_URL; } logger.info("Sending password reset email", { user, url }); const emailPasswordRequest = await renderPasswordResetRequestEmail({ baseUrl: env.BASE_URL, name: user.name, resetLink: url, period: SHORT_LIVED_TOKEN, }); await emailSender({ toEmail: user.email, subject: "Password Reset Request", html: emailPasswordRequest, text: `Hi ${user.name || "User"}. You can reset your password by clicking the link below: ${url}.`, }); } catch (error) { logger.error("Error sending reset password email", { error, user }); throw error; } }, }, socialProviders: { google: { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, prompt: "select_account", redirectURI: `${env.PRODUCTION_URL}/api/auth/callback/google`, }, /* TODO Add Apple provider */ }, emailVerification: { onEmailVerification: async (user) => { try { // Add comprehensive safety checks for the user object if (!user) { logger.error("onEmailVerification: user object is null or undefined"); return; } if (!user.email) { logger.error("onEmailVerification: user.email is missing", { user }); return; } // Ensure user.name exists, fallback to user.email username or "User" if name is not available const userName = user.name || user.email?.split("@")[0] || "User"; logger.info("Processing email verification", { userId: user.id, email: user.email, name: userName, userObject: user, }); const email = await renderAccountVerifiedEmail({ baseUrl: env.BASE_URL, name: userName, }); await emailSender({ toEmail: user.email, subject: "Account Verified", html: email, text: `Hi ${userName}. Your account has been successfully verified. You can now log in and start using our services.`, }); logger.info("Email verification successful", { userId: user.id, email: user.email, }); } catch (error) { logger.error("Error in email verification callback", { error, user: user || "user is null/undefined", }); // Don't re-throw the error to prevent blocking the verification process } }, sendVerificationEmail: async ({ url, user }) => { try { if (!url.includes("verification=true")) { if (url.includes("?")) { url = url + "&verification=true"; } else { url = url + "?verification=true"; } } // Ensure user.name exists, fallback to user.email username or "User" if name is not available const userName = user.name || user.email?.split("@")[0] || "User"; logger.info("Sending verification email", { userId: user.id, email: user.email, name: userName, url, }); const email = await renderAccountVerificationEmail({ baseUrl: env.BASE_URL, name: userName, verificationLink: url, }); await emailSender({ toEmail: user.email, subject: "Account Verification", html: email, text: `Hi ${userName}. Please click the link below to verify your account: ${url}`, }); logger.info("Verification email sent successfully", { userId: user.id, email: user.email, }); } catch (error) { logger.error("Error sending verification email", { error, user }); throw error; } }, async afterEmailVerification(user, request) { logger.info("Email verified successfully", { user }); }, sendOnSignUp: true, }, verification: { disableCleanup: env.NODE_ENV === "development", verificationTokenExpiresIn: 24 * 60 * 60, // 24 hours in seconds }, /* SECTION: User Settings */ user: { changeEmail: { enabled: true, sendChangeEmailVerification: async ({ user }) => { logger.debug("Sending change email verification", { user }); }, }, deleteUser: { enabled: true, deleteTokenExpiresIn: SHORT_LIVED_TOKEN, beforeDelete: async (user) => { logger.info("User deletion started", { user }); }, afterDelete: async (user) => { logger.info("User deletion completed", { user }); }, sendDeleteAccountVerification: async ({ user }) => { logger.debug("Sending delete account verification", { user }); }, }, }, account: { accountLinking: { enabled: true, allowDifferentEmails: true, allowUnlinkingAll: false, trustedProviders: ["google"], }, encryptOAuthTokens: env.NODE_ENV === "development" ? false : true, updateAccountOnSignIn: true, }, session: { freshAge: FRESH_TOKEN, expiresIn: undefined, cookieCache: { enabled: true, maxAge: SHORT_LIVED_TOKEN, }, storeSessionInDatabase: true, }, /* SECTION: Plugins */ plugins: [ username({ displayUsernameNormalization: (username: string) => { return username .trim() .replace(/\s+/g, "_") .replace(/[^a-zA-Z0-9_-]/g, ""); }, usernameNormalization: (username: string) => { return username .trim() .replace(/\s+/g, "_") .replace(/[^a-zA-Z0-9_-]/g, ""); }, displayUsernameValidator: (username: string) => { return USERNAME_REGEX.test(username); }, usernameValidator: (username: string) => { return USERNAME_REGEX.test(username); }, maxUsernameLength: MAX_USERNAME_LENGTH, minUsernameLength: MIN_USERNAME_LENGTH, validationOrder: { username: "pre-normalization", displayUsername: "pre-normalization", }, }), phoneNumber({ sendOTP: async ({ phoneNumber }) => { logger.debug("Sending OTP", { phoneNumber }); }, requireVerification: false, }), admin({ ac: adminAc, roles: { idp_super_admin, idp_system_admin, idp_security_admin, idp_user_manager, idp_developer, idp_support_agent, idp_auditor, idp_user, }, adminRoles, }), organization({ ac: tempOrgAc, dynamicAccessControl: { enabled: true, }, creatorRole: "owner", invitationLimit: ({}) => { return 100; }, allowUserToCreateOrganization: async ({ emailVerified }) => { return emailVerified; }, cancelPendingInvitationsOnReInvite: true, disableOrganizationDeletion: false, invitationExpiresIn: LONG_LIVED_TOKEN, teams: { enabled: true, allowRemovingAllTeams: false, defaultTeam: { enabled: false, }, }, membershipLimit: 1000, organizationLimit: 5, requireEmailVerificationOnInvitation: true, sendInvitationEmail: async ({ email, inviter, organization }) => { logger.debug("Sending invitation email", { email }); const emailInvite = await renderInviteEmail({ baseUrl: env.BASE_URL, inviteLink: `${env.BASE_URL}/`, inviterEmail: inviter.user.email, inviterName: inviter.user.name, teamName: organization.name, name: email, }); await emailSender({ toEmail: email, subject: "Invitation to join organization", html: emailInvite, }); }, }), apiKey({ defaultKeyLength: API_KEY_LENGTH, maximumNameLength: API_KEY_MAX_NAME_LENGTH, minimumNameLength: API_KEY_MIN_NAME_LENGTH, defaultPrefix: "GXS_API-", enableMetadata: true, disableKeyHashing: env.NODE_ENV === "development", requireName: true, startingCharactersConfig: { charactersLength: 12, shouldStore: true, }, }), twoFactor({ issuer: APP_SLUG, backupCodeOptions: { amount: BACKUP_CODES_AMMOUNT, length: BACKUP_CODES_LENGTH, storeBackupCodes: env.NODE_ENV === "development" ? "plain" : "encrypted", }, otpOptions: { allowedAttempts: MAX_OTP_ATTEMPTS, digits: OTP_LENGTH, period: SHORT_LIVED_TOKEN, storeOTP: env.NODE_ENV === "development" ? "plain" : "hashed", sendOTP: async ({ user, otp }) => { try { const otpEmail = await renderOneTimePasswordEmail({ baseUrl: env.BASE_URL, name: user.name, otp, period: SHORT_LIVED_TOKEN, }); await emailSender({ toEmail: user.email, toName: user.name, html: otpEmail, subject: "Your One-Time Password (OTP)", }); } catch (error) { logger.error("Error Sending OTP", error); throw error; } }, }, totpOptions: { digits: OTP_LENGTH, }, skipVerificationOnEnable: false, }), passkey({ rpID: APP_SLUG, rpName: APP_NAME, origin: env.BASE_URL, }), lastLoginMethod({ storeInDatabase: true, customResolveMethod: (ctx) => { if (ctx.path === "/oauth2/authorize") { return "oauth2"; } return null; }, }), bearer(), openAPI(), oAuthProxy({ currentURL: env.BASE_URL, productionURL: env.PRODUCTION_URL, }), jwt({ jwks: { keyPairConfig: { alg: "RS256", }, }, }), ], /* SECTION: Advanced */ logger: { level: env.LOG_LEVEL, disableColors: false, disabled: false, log: (level, message, meta) => { const cleanMeta = meta && typeof meta === "object" && meta !== null ? Object.fromEntries( Object.entries(meta).filter(([k]) => k !== "ctx"), ) : meta; if (level === "error") { logger.error( message, env.LOG_LEVEL === "debug" ? cleanMeta : undefined, ); } else if (level === "warn") { logger.warn(message, env.LOG_LEVEL === "debug" ? cleanMeta : undefined); } else if (level === "info") { logger.info(message, env.LOG_LEVEL === "debug" ? cleanMeta : undefined); } else if (level === "debug") { logger.debug(message, cleanMeta); } else { logger.info(message, { level, internalMessage: "A log level that is not handled was created", ...cleanMeta, }); } }, }, advanced: { ipAddress: { disableIpTracking: false, }, database: { defaultFindManyLimit: 100, }, }, databaseHooks: { user: { create: { before: async (user) => { logger.debug("User to be created", { user }); }, after: async (user) => { logger.debug("User created", { user }); }, }, }, verification: { create: { before: async (data) => { logger.debug("Verification to be created", { data }); }, }, }, }, onAPIError: { errorURL: "/error", onError: (error) => { if (error instanceof Error && error.message) { logger.error(error.message, { error }); } logger.error("An API error occurred", { error }); }, throw: false, }, rateLimit: { enabled: env.NODE_ENV === "development" ? false : true, storage: "secondary-storage", window: 60 * 1, // 1 minute max: 25, }, }); ``` ### Additional context This [discord issue](https://discord.com/channels/1288403910284935179/1428153898090106940) details what could be happening. Tldr: <img width="1212" height="359" alt="Image" src="https://github.com/user-attachments/assets/8f07e674-692b-4191-b8af-ab483faccd79" />
GiteaMirror added the nextjs label 2026-03-13 09:33:58 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Oct 24, 2025):

Hi @MarioPon11! I'm Dosu and I’m helping the better-auth team.

The root cause of the please_restart_the_process error is browser-level cookie isolation: cookies set on localhost or a preview domain during the initial OAuth request are not available on the production domain during the callback, so state verification fails—even when using the oAuthProxy plugin and setting skipStateCookieCheck to true. This is a browser security restriction and not a bug in Better Auth or the plugin itself see discussion.

The oAuthProxy plugin tries to proxy requests and skip state cookie checks, but it cannot bypass browser rules that prevent cookies from being shared across domains (especially public suffixes like .azurewebsites.net or .cloudflare.dev). The only reliable workaround is to use a custom domain for both production and development environments and set your cookie domain accordingly (e.g., .yourdomain.com). This allows cookies to be shared across subdomains and enables the OAuth flow to work as intended see details.

Other things to double-check: make sure all relevant URLs are in trustedOrigins, register all callback URLs with your OAuth provider, set the productionURL option correctly in oAuthProxy, and ensure preview/local environments share the same database for verification state see recommendations.

If you need to support public suffix domains, there is currently no workaround—this is a browser-level limitation. If you want to avoid double-callback and state errors, ensure the callbackURL passed to your client-side sign-in (e.g., signInSocial) is a user-facing route (like /), not the API callback endpoint see explanation.

Let me know if you need help with specific config changes or domain setup!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Oct 24, 2025): <!-- Greeting --> Hi @MarioPon11! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> The root cause of the `please_restart_the_process` error is browser-level cookie isolation: cookies set on localhost or a preview domain during the initial OAuth request are not available on the production domain during the callback, so state verification fails—even when using the oAuthProxy plugin and setting `skipStateCookieCheck` to true. This is a browser security restriction and not a bug in Better Auth or the plugin itself [see discussion](https://github.com/better-auth/better-auth/issues/5073). The oAuthProxy plugin tries to proxy requests and skip state cookie checks, but it cannot bypass browser rules that prevent cookies from being shared across domains (especially public suffixes like `.azurewebsites.net` or `.cloudflare.dev`). The only reliable workaround is to use a custom domain for both production and development environments and set your cookie domain accordingly (e.g., `.yourdomain.com`). This allows cookies to be shared across subdomains and enables the OAuth flow to work as intended [see details](https://github.com/better-auth/better-auth/issues/5073). Other things to double-check: make sure all relevant URLs are in `trustedOrigins`, register all callback URLs with your OAuth provider, set the `productionURL` option correctly in oAuthProxy, and ensure preview/local environments share the same database for verification state [see recommendations](https://github.com/better-auth/better-auth/issues/4985). If you need to support public suffix domains, there is currently no workaround—this is a browser-level limitation. If you want to avoid double-callback and state errors, ensure the callbackURL passed to your client-side sign-in (e.g., `signInSocial`) is a user-facing route (like `/`), not the API callback endpoint [see explanation](https://github.com/better-auth/better-auth/issues/3488). Let me know if you need help with specific config changes or domain setup! <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/50a17d33-2284-40b2-8569-33bc7a91fabc?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5563)
Author
Owner

@ryunosuke21 commented on GitHub (Oct 24, 2025):

@dosu I am using a custom domain. It along the lines of https://auth.example.com, and the proxy is giving issues on both localhost and things like https://pr-1.auth.example.com, which is a preview environment. The database is being shared between all the hosts.

@ryunosuke21 commented on GitHub (Oct 24, 2025): @dosu I am using a custom domain. It along the lines of `https://auth.example.com`, and the proxy is giving issues on both `localhost` and things like `https://pr-1.auth.example.com`, which is a preview environment. The database is being shared between all the hosts.
Author
Owner

@dosubot[bot] commented on GitHub (Oct 24, 2025):

The proxy issues you're seeing on both localhost and preview environments—even with a custom domain and shared database—are almost always due to browser cookie isolation and how state/cookie validation is handled in multi-domain OAuth flows. Even with the oAuthProxy plugin, cookies set on one subdomain (or localhost) aren't available on another, so state verification fails unless cookies are configured to be shared across all subdomains.

Here’s what you should check and update:

  • Make sure your cookie settings explicitly set the domain to .example.com (not just auth.example.com), and set SameSite=None and Secure=true for the OAuth state cookie. This allows cookies to be shared across all subdomains and enables the proxy flow to work as intended see details.
  • In your Better Auth config, add:
    advanced: {
      cookies: {
        state: {
          attributes: {
            domain: ".example.com",
            sameSite: "none",
            secure: true,
          }
        }
      }
    }
    
  • Ensure all preview URLs (like https://pr-1.auth.example.com) and localhost are included in trustedOrigins see guidance.
  • Register all relevant callback URLs (localhost, preview, production) with your OAuth provider (Google) see explanation.
  • Always initiate social sign-in from the client (not server/API routes) using the Better Auth client SDK. The callbackURL you pass to signInSocial should be a user-facing route (like /), not the API callback endpoint see best practice.
  • The oAuthProxy plugin in recent versions (including 1.3.30) automatically skips the state cookie check during proxying, but this is only safe for dev/staging and has security implications see PR.

If you’ve already set all this up and still see issues, double-check your cookie domain and attributes, and use browser devtools to confirm cookies are being set and sent on all relevant domains. If you need more help, let me know your exact cookie config and any error logs you’re seeing.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other  Chat with Dosu Join Discord Share on X

@dosubot[bot] commented on GitHub (Oct 24, 2025): <!-- Answer --> The proxy issues you're seeing on both localhost and preview environments—even with a custom domain and shared database—are almost always due to browser cookie isolation and how state/cookie validation is handled in multi-domain OAuth flows. Even with the oAuthProxy plugin, cookies set on one subdomain (or localhost) aren't available on another, so state verification fails unless cookies are configured to be shared across all subdomains. Here’s what you should check and update: - Make sure your cookie settings explicitly set the domain to `.example.com` (not just `auth.example.com`), and set `SameSite=None` and `Secure=true` for the OAuth state cookie. This allows cookies to be shared across all subdomains and enables the proxy flow to work as intended [see details](https://github.com/better-auth/better-auth/issues/5478). - In your Better Auth config, add: ```ts advanced: { cookies: { state: { attributes: { domain: ".example.com", sameSite: "none", secure: true, } } } } ``` - Ensure all preview URLs (like `https://pr-1.auth.example.com`) and localhost are included in `trustedOrigins` [see guidance](https://github.com/better-auth/better-auth/issues/5073). - Register all relevant callback URLs (localhost, preview, production) with your OAuth provider (Google) [see explanation](https://github.com/better-auth/better-auth/issues/4953). - Always initiate social sign-in from the client (not server/API routes) using the Better Auth client SDK. The callbackURL you pass to `signInSocial` should be a user-facing route (like `/`), not the API callback endpoint [see best practice](https://github.com/better-auth/better-auth/issues/3488). - The oAuthProxy plugin in recent versions (including 1.3.30) automatically skips the state cookie check during proxying, but this is only safe for dev/staging and has security implications [see PR](https://github.com/better-auth/better-auth/pull/4991). If you’ve already set all this up and still see issues, double-check your cookie domain and attributes, and use browser devtools to confirm cookies are being set and sent on all relevant domains. If you need more help, let me know your exact cookie config and any error logs you’re seeing. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/df97ebcc-2b10-4e92-81d8-d188b15d5203?feedback_type=other)</sup>&nbsp;&nbsp;[![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/5563)
Author
Owner

@bytaesu commented on GitHub (Nov 2, 2025):

Hi,

I’ve rewritten the logic for the OAuth Proxy. The new implementation includes:

  • Resolved state-related errors
  • Resolved cross-origin issues (e.g. Vercel preview)
  • ... other logic improvements

The issue will be automatically closed once the PR is merged. If the problem persists afterward, please feel free to open a new issue!

@bytaesu commented on GitHub (Nov 2, 2025): Hi, I’ve rewritten the logic for the OAuth Proxy. The new implementation includes: - Resolved state-related errors - Resolved cross-origin issues (e.g. Vercel preview) - ... other logic improvements The issue will be automatically closed once the PR is merged. If the problem persists afterward, please feel free to open a new issue!
Author
Owner

@better-auth-agent[bot] commented on GitHub (Nov 2, 2025):

It looks like the issue was recognized and addressed recently. The OAuth Proxy’s rewritten logic now properly handles state validation to prevent the “please_restart_the_process” error from social providers (in your case, Google) and resolves related cross-origin issues. The recommended next step is to update to the latest commit or release of Better Auth that includes these changes. After updating, please retest your social sign‐in flow on your VPS. If you still experience problems, then consider opening a new issue with your logs and environment details so we can further investigate [1].

DiagramDiscordGitHub

Diagram Join Star

@better-auth-agent[bot] commented on GitHub (Nov 2, 2025): It looks like the issue was recognized and addressed recently. The OAuth Proxy’s rewritten logic now properly handles state validation to prevent the “please_restart_the_process” error from social providers (in your case, Google) and resolves related cross-origin issues. The recommended next step is to update to the latest commit or release of Better Auth that includes these changes. After updating, please retest your social sign‐in flow on your VPS. If you still experience problems, then consider opening a new issue with your logs and environment details so we can further investigate [[1]](https://github.com/better-auth/better-auth/issues/5563). <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) • [Discord](https://discord.gg/better-auth) • [GitHub](https://github.com/better-auth/better-auth) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/better-auth_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/better-auth) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/better-auth/better-auth)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2200