[GH-ISSUE #7041] Error: Access-Control-Allow-Origin * when i was redirected from chatgpt to login screen of my app #10712

Closed
opened 2026-04-13 07:01:08 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @satish-buildin2 on GitHub (Dec 28, 2025).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7041

Originally assigned to: @gustavovalverde, @Paola3stefania on GitHub.

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

Description

When navigating to the login page normally, email/password sign-in works as expected:

const res = await authClient.signIn.email({
  email: values.email,
  password: values.password,
  rememberMe: values.rememberMe,
});

However, when the login page is opened via an OAuth redirect from ChatGPT, the same sign-in request fails with a CORS credentials error, even though authentication actually succeeds on the backend.


OAuth Redirect URL

https://staging.myapp.app/auth/login
  ?response_type=code
  &client_id=KRYfXYMwtuhPcdwQyRTRxQDKKSiOaTrL
  &redirect_uri=https%3A%2F%2Fchatgpt.com%2Fconnector_platform_oauth_redirect
  &state=oauth_s_69510c39f2d481919ac2add74f0dafc5
  &scope=openid+profile+email+offline_access
  &code_challenge=KrU_BCieDDmLwD0jqjS7wIsNmzBRu7VZo_v6fgyfgWg
  &code_challenge_method=S256
  &resource=https%3A%2F%2Fapi-staging.myapp.app%2Fapi%2Fmcp

Error Observed (Browser Console)

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at
https://api-staging.myapp.app/api/auth/sign-in/email.

Reason: Credential is not supported if the CORS header
‘Access-Control-Allow-Origin’ is ‘*’.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource.
Reason: CORS request did not succeed. Status code: (null).

Additional Observations

  • After the error occurs, refreshing the page shows the user as already logged in

  • Session cookies are successfully set despite the browser blocking the response

  • Works fine when:

  • Navigating directly to /auth/login

  • Opening the OAuth link in Incognito / another browser

  • Running the same flow on a local development instance

  • Fails when:

  • Redirected to the login page from ChatGPT OAuth flow

  • The same frontend code and same API endpoint are used in all cases


Network Request Details

Request

POST https://api-staging.myapp.app/api/auth/sign-in/email
Origin: https://staging.myapp.app

Response Headers

access-control-allow-credentials: true
access-control-allow-origin: *
access-control-allow-methods: POST, OPTIONS
access-control-allow-headers: Content-Type, Authorization

Status

302 Found
Location: /api/auth/error?error=invalid_client

Cookies Set

__Secure-better-auth.session_token; SameSite=None; Secure; HttpOnly
__Secure-better-auth.dont_remember; SameSite=None; Secure; HttpOnly

Environment

  • Frontend: https://staging.myapp.app
  • API: https://api-staging.myapp.app
  • Local environment: works correctly
  • Browser: Firefox 146 (Linux)
  • Auth flow: OAuth 2.0 + PKCE (ChatGPT connector)

Current vs. Expected behavior

current:
cors error

expected:

Login via OAuth redirect (ChatGPT → myapp → API) should complete without CORS errors and return a readable response to the frontend.

What version of Better Auth are you using?

1.4.6

System info

{
  "system": {
    "platform": "linux",
    "arch": "x64",
    "version": "#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2",
    "release": "6.14.0-37-generic",
    "cpuCount": 16,
    "cpuModel": "12th Gen Intel(R) Core(TM) i5-1240P",
    "totalMemory": "15.25 GB",
    "freeMemory": "2.19 GB"
  },
  "node": {
    "version": "v24.11.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^18.3.1"
    },
    {
      "name": "express",
      "version": "^5.2.1"
    }
  ],
  "databases": null,
  "betterAuth": {
    "version": "^1.4.6",
    "config": {
      "trustedOrigins": [
        "http://localhost:5173",
        "https://chatgpt.com"
      ],
      "secret": "[REDACTED]",
      "baseURL": "https://skinlike-unmutinous-maddox.ngrok-free.dev",
      "errorURL": "http://localhost:5173",
      "user": {
        "additionalFields": {
          "isOnboardingComplete": {
            "type": "boolean",
            "defaultValue": false,
            "required": false
          }
        }
      },
      "account": {
        "accountLinking": {
          "enabled": true,
          "allowDifferentEmails": false,
          "updateUserInfoOnLink": true
        }
      },
      "plugins": [
        {
          "name": "admin",
          "config": {
            "id": "admin",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "$ERROR_CODES": {
              "FAILED_TO_CREATE_USER": "Failed to create user",
              "USER_ALREADY_EXISTS": "User already exists.",
              "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.",
              "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself",
              "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role",
              "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users",
              "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users",
              "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users",
              "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions",
              "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users",
              "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]",
              "BANNED_USER": "You have been banned from this application",
              "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user",
              "NO_DATA_TO_UPDATE": "No data to update",
              "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users",
              "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself",
              "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value",
              "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins"
            },
            "schema": {
              "user": {
                "fields": {
                  "role": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banned": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false,
                    "input": false
                  },
                  "banReason": {
                    "type": "string",
                    "required": false,
                    "input": false
                  },
                  "banExpires": {
                    "type": "date",
                    "required": false,
                    "input": false
                  }
                }
              },
              "session": {
                "fields": {
                  "impersonatedBy": {
                    "type": "string",
                    "required": false
                  }
                }
              }
            },
            "options": {
              "defaultRole": "user",
              "adminRoles": [
                "admin"
              ]
            }
          }
        },
        {
          "name": "mcp",
          "config": {
            "id": "mcp",
            "hooks": {
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthApplication": {
                "modelName": "oauthApplication",
                "fields": {
                  "name": {
                    "type": "string"
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "string",
                    "required": false
                  },
                  "clientId": {
                    "type": "string",
                    "unique": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "redirectUrls": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "disabled": {
                    "type": "boolean",
                    "required": false,
                    "defaultValue": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "accessToken": {
                    "type": "string",
                    "unique": true
                  },
                  "refreshToken": {
                    "type": "string",
                    "unique": true
                  },
                  "accessTokenExpiresAt": {
                    "type": "date"
                  },
                  "refreshTokenExpiresAt": {
                    "type": "date"
                  },
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "references": {
                      "model": "oauthApplication",
                      "field": "clientId",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "userId": {
                    "type": "string",
                    "references": {
                      "model": "user",
                      "field": "id",
                      "onDelete": "cascade"
                    },
                    "index": true
                  },
                  "scopes": {
                    "type": "string"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  },
                  "consentGiven": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        }
      ],
      "emailAndPassword": {
        "enabled": true,
        "requireEmailVerification": true
      },
      "emailVerification": {
        "sendOnSignUp": true,
        "autoSignInAfterVerification": true
      },
      "session": {
        "expiresIn": 604800,
        "updateAge": 86400
      },
      "advanced": {
        "defaultCookieAttributes": {
          "sameSite": "none",
          "secure": true,
          "httpOnly": true
        }
      },
      "socialProviders": {
        "google": {
          "prompt": "select_account consent",
          "clientId": "[REDACTED]",
          "clientSecret": "[REDACTED]",
          "scope": [
            "openid",
            "email",
            "profile",
            "https://www.googleapis.com/auth/gmail.send"
          ],
          "accessType": "offline"
        }
      },
      "databaseHooks": {
        "account": {
          "create": {}
        }
      },
      "hooks": {}
    }
  }
}

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

Backend, Client

Auth config (if applicable)

import {
  BETTER_AUTH_SECRET,
  BETTER_AUTH_URL,
  CORS_ORIGIN,
  GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET,
  IS_PROD,
  REQUIRE_EMAIL_VERIFICATION,
} from "@/config/constants";
import {
  addProviderEmail,
  ProviderId,
} from "@/features/user/services/addProviderEmail.service";
import { client } from "@/lib/mongo";
import { userRoleEnum } from "@packages/shared/schemas/user";
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import {
  admin,
  createAuthMiddleware,
  mcp,
  oAuthDiscoveryMetadata,
  oAuthProtectedResourceMetadata,
} from "better-auth/plugins";
import { sendResetPasswordEmail, sendVerificationEmail } from "./resend";

export const auth = betterAuth({
  trustedOrigins: [CORS_ORIGIN,"https://chatgpt.com"],
  database: mongodbAdapter(client),
  secret: BETTER_AUTH_SECRET,
  baseURL: BETTER_AUTH_URL,
  errorURL: CORS_ORIGIN,
  user: {
    additionalFields: {
      isOnboardingComplete: {
        type: "boolean",
        defaultValue: false,
        required: false,
      },
    },
  },
  account: {
    accountLinking: {
      enabled: true,
      allowDifferentEmails: false,
      updateUserInfoOnLink: true,
    },
  },
  plugins: [
    admin({
      defaultRole: userRoleEnum.USER,
      adminRoles: [userRoleEnum.ADMIN],
    }),
    mcp({
      loginPage: `${CORS_ORIGIN}/auth/login`,
      resource: `${BETTER_AUTH_URL}/api/mcp`,
      oidcConfig: {
        loginPage: `${CORS_ORIGIN}/auth/login`,
        allowDynamicClientRegistration: true,
      },
    }),
  ],

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: REQUIRE_EMAIL_VERIFICATION,
    sendResetPassword: async ({ user, url, token }, request) => {
      await sendResetPasswordEmail(user, url);
    },
  },
  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url, token }, request) => {
      await sendVerificationEmail(user, url);
    },
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // 1 day
  },
  advanced: {
    defaultCookieAttributes: {
      sameSite: "none",
      secure: true,
      httpOnly: true,
    },
  },
  socialProviders: {
    google: {
      prompt: "select_account consent",
      clientId: GOOGLE_CLIENT_ID,
      clientSecret: GOOGLE_CLIENT_SECRET,
      scope: [
        "openid",
        "email",
        "profile",
        "https://www.googleapis.com/auth/gmail.send",
      ],
      accessType: "offline", // Required to get refresh tokens
    },
    // microsoft: {
    //   clientId: MICROSOFT_CLIENT_ID,
    //   clientSecret: MICROSOFT_CLIENT_SECRET,
    //   tenantId: "common",
    //   prompt: "select_account",
    // },
  },
  databaseHooks: {
    account: {
      create: {
        after: async (account) => {
          // skip if providerId is credentials
          if (account.providerId === "credentials") {
            return;
          }

          // Use service to add provider email
          await addProviderEmail(
            account.id,
            account.providerId as ProviderId,
            account.accessToken || undefined
          );
        },
      },
    },
  },
  hooks: {
    before: createAuthMiddleware(async (ctx) => {
      if (ctx.path === "/error") {
        const { error } = ctx.query as { error?: string };

        let redirectUrl = `${CORS_ORIGIN}/?error=${encodeURIComponent(
          "Authentication Error"
        )}&error_description=${encodeURIComponent("Failed to authenticate")}`;

        if (error === "banned") {
          redirectUrl = `${CORS_ORIGIN}/?error=${encodeURIComponent(
            "You have been banned"
          )}&error_description=${encodeURIComponent("Contact support team")}`;
        }

        throw ctx.redirect(redirectUrl);
      }
    }),
  },
});

// Use the shared types for consistency
export type Session = typeof auth.$Infer.Session.session;
export type User = typeof auth.$Infer.Session.user;

Additional context

const app = express();
app.use(morgan("dev"));

const allowedOrigins = [
CORS_ORIGIN,
"https://chatgpt.com",
];

app.use(
cors({
origin(origin, callback) {
// Debug: log the incoming origin
console.log("Incoming origin:", origin);
console.log("Allowed origins:", allowedOrigins);

  // allow requests with no origin (like from mobile apps, curl, postman)
  if (!origin) return callback(null, true);
  
  if (allowedOrigins.includes(origin)) {
    callback(null, true);
  } else {
    console.log("CORS blocked origin:", origin);
    callback(new Error("CORS policy does not allow this origin"));
  }
},
credentials: true,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
exposedHeaders: ["WWW-Authenticate"],

})
);

// make sure cors middleware runs BEFORE JSON parser and routes

app.use(express.json());

app.all("/api/auth/*path", toNodeHandler(auth));

// Add JSON parsing middleware for webhook
// Note: MCP handler handles its own body parsing, so express.json() won't interfere

// Redirect all /.well-known/* requests to /api/auth/.well-known/* for consistency
app.all("/.well-known/*path", (req, res) => {
const path = req.path.replace("/.well-known", "");
const query = req.url.includes("?") ? req.url.substring(req.url.indexOf("?")) : "";
res.redirect(307, /api/auth/.well-known${path}${query});
});

// MCP endpoints must be registered BEFORE body parser
// Handler will be initialized in runHealthAndStart before server starts
let mcpHandler: any = null;

// Register MCP route with body parsing middleware
// Use express.json() specifically for MCP route (before global express.json())
app.all("/api/mcp", (req, res, next) => {
const requestId = req.body?.id || 'null';
logger.info([MCP_ROUTE] Request received: ${req.method} ${req.url} (Request ID: ${requestId}), "MCP_ROUTE");
logger.info([MCP_ROUTE] Body type: ${typeof req.body}, keys: ${req.body ? Object.keys(req.body).join(',') : 'none'}, "MCP_ROUTE");

if (!mcpHandler) {
logger.warn("[MCP_ROUTE] Handler not initialized yet", "MCP_ROUTE");
if (!res.headersSent) {
res.json({
jsonrpc: "2.0",
id: null,
error: {
code: -32000,
message: "Server Error",
data: "MCP handler not initialized. Server is still starting up.",
},
});
}
return;
}

// Call the handler - it's already an Express middleware from withMcpAuth
logger.info("[MCP_ROUTE] Calling MCP handler", "MCP_ROUTE");
return mcpHandler(req, res, next);
});

Originally created by @satish-buildin2 on GitHub (Dec 28, 2025). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7041 Originally assigned to: @gustavovalverde, @Paola3stefania on GitHub. ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce ### Description When navigating to the login page normally, email/password sign-in works as expected: ```ts const res = await authClient.signIn.email({ email: values.email, password: values.password, rememberMe: values.rememberMe, }); ``` However, when the login page is opened **via an OAuth redirect from ChatGPT**, the same sign-in request fails with a **CORS credentials error**, even though authentication actually succeeds on the backend. --- ### OAuth Redirect URL ``` https://staging.myapp.app/auth/login ?response_type=code &client_id=KRYfXYMwtuhPcdwQyRTRxQDKKSiOaTrL &redirect_uri=https%3A%2F%2Fchatgpt.com%2Fconnector_platform_oauth_redirect &state=oauth_s_69510c39f2d481919ac2add74f0dafc5 &scope=openid+profile+email+offline_access &code_challenge=KrU_BCieDDmLwD0jqjS7wIsNmzBRu7VZo_v6fgyfgWg &code_challenge_method=S256 &resource=https%3A%2F%2Fapi-staging.myapp.app%2Fapi%2Fmcp ``` --- ### Error Observed (Browser Console) ``` Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api-staging.myapp.app/api/auth/sign-in/email. Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’. ``` ``` Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource. Reason: CORS request did not succeed. Status code: (null). ``` --- ### Additional Observations * **After the error occurs, refreshing the page shows the user as already logged in** * Session cookies are successfully set despite the browser blocking the response * Works fine when: * Navigating directly to `/auth/login` * Opening the OAuth link in **Incognito / another browser** * Running the same flow on a **local development instance** * Fails when: * Redirected to the login page **from ChatGPT OAuth flow** * The **same frontend code** and **same API endpoint** are used in all cases --- ### Network Request Details **Request** ``` POST https://api-staging.myapp.app/api/auth/sign-in/email Origin: https://staging.myapp.app ``` **Response Headers** ``` access-control-allow-credentials: true access-control-allow-origin: * access-control-allow-methods: POST, OPTIONS access-control-allow-headers: Content-Type, Authorization ``` **Status** ``` 302 Found Location: /api/auth/error?error=invalid_client ``` **Cookies Set** ``` __Secure-better-auth.session_token; SameSite=None; Secure; HttpOnly __Secure-better-auth.dont_remember; SameSite=None; Secure; HttpOnly ``` --- ### Environment * Frontend: `https://staging.myapp.app` * API: `https://api-staging.myapp.app` * Local environment: works correctly * Browser: Firefox 146 (Linux) * Auth flow: OAuth 2.0 + PKCE (ChatGPT connector) ### Current vs. Expected behavior current: cors error expected: Login via OAuth redirect (ChatGPT → myapp → API) should complete without CORS errors and return a readable response to the frontend. ### What version of Better Auth are you using? 1.4.6 ### System info ```bash { "system": { "platform": "linux", "arch": "x64", "version": "#37~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 10:25:38 UTC 2", "release": "6.14.0-37-generic", "cpuCount": 16, "cpuModel": "12th Gen Intel(R) Core(TM) i5-1240P", "totalMemory": "15.25 GB", "freeMemory": "2.19 GB" }, "node": { "version": "v24.11.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "react", "version": "^18.3.1" }, { "name": "express", "version": "^5.2.1" } ], "databases": null, "betterAuth": { "version": "^1.4.6", "config": { "trustedOrigins": [ "http://localhost:5173", "https://chatgpt.com" ], "secret": "[REDACTED]", "baseURL": "https://skinlike-unmutinous-maddox.ngrok-free.dev", "errorURL": "http://localhost:5173", "user": { "additionalFields": { "isOnboardingComplete": { "type": "boolean", "defaultValue": false, "required": false } } }, "account": { "accountLinking": { "enabled": true, "allowDifferentEmails": false, "updateUserInfoOnLink": true } }, "plugins": [ { "name": "admin", "config": { "id": "admin", "hooks": { "after": [ {} ] }, "endpoints": {}, "$ERROR_CODES": { "FAILED_TO_CREATE_USER": "Failed to create user", "USER_ALREADY_EXISTS": "User already exists.", "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL": "User already exists. Use another email.", "YOU_CANNOT_BAN_YOURSELF": "You cannot ban yourself", "YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE": "You are not allowed to change users role", "YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS": "You are not allowed to create users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS": "You are not allowed to list users", "YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS": "You are not allowed to list users sessions", "YOU_ARE_NOT_ALLOWED_TO_BAN_USERS": "You are not allowed to ban users", "YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS": "You are not allowed to impersonate users", "YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS": "You are not allowed to revoke users sessions", "YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS": "You are not allowed to delete users", "YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD": "[REDACTED]", "BANNED_USER": "You have been banned from this application", "YOU_ARE_NOT_ALLOWED_TO_GET_USER": "You are not allowed to get user", "NO_DATA_TO_UPDATE": "No data to update", "YOU_ARE_NOT_ALLOWED_TO_UPDATE_USERS": "You are not allowed to update users", "YOU_CANNOT_REMOVE_YOURSELF": "You cannot remove yourself", "YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE": "You are not allowed to set a non-existent role value", "YOU_CANNOT_IMPERSONATE_ADMINS": "You cannot impersonate admins" }, "schema": { "user": { "fields": { "role": { "type": "string", "required": false, "input": false }, "banned": { "type": "boolean", "defaultValue": false, "required": false, "input": false }, "banReason": { "type": "string", "required": false, "input": false }, "banExpires": { "type": "date", "required": false, "input": false } } }, "session": { "fields": { "impersonatedBy": { "type": "string", "required": false } } } }, "options": { "defaultRole": "user", "adminRoles": [ "admin" ] } } }, { "name": "mcp", "config": { "id": "mcp", "hooks": { "after": [ {} ] }, "endpoints": {}, "schema": { "oauthApplication": { "modelName": "oauthApplication", "fields": { "name": { "type": "string" }, "icon": { "type": "string", "required": false }, "metadata": { "type": "string", "required": false }, "clientId": { "type": "string", "unique": true }, "clientSecret": { "type": "string", "required": false }, "redirectUrls": { "type": "string" }, "type": { "type": "string" }, "disabled": { "type": "boolean", "required": false, "defaultValue": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "accessToken": { "type": "string", "unique": true }, "refreshToken": { "type": "string", "unique": true }, "accessTokenExpiresAt": { "type": "date" }, "refreshTokenExpiresAt": { "type": "date" }, "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" }, "index": true }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "references": { "model": "oauthApplication", "field": "clientId", "onDelete": "cascade" }, "index": true }, "userId": { "type": "string", "references": { "model": "user", "field": "id", "onDelete": "cascade" }, "index": true }, "scopes": { "type": "string" }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" }, "consentGiven": { "type": "boolean" } } } } } } ], "emailAndPassword": { "enabled": true, "requireEmailVerification": true }, "emailVerification": { "sendOnSignUp": true, "autoSignInAfterVerification": true }, "session": { "expiresIn": 604800, "updateAge": 86400 }, "advanced": { "defaultCookieAttributes": { "sameSite": "none", "secure": true, "httpOnly": true } }, "socialProviders": { "google": { "prompt": "select_account consent", "clientId": "[REDACTED]", "clientSecret": "[REDACTED]", "scope": [ "openid", "email", "profile", "https://www.googleapis.com/auth/gmail.send" ], "accessType": "offline" } }, "databaseHooks": { "account": { "create": {} } }, "hooks": {} } } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { BETTER_AUTH_SECRET, BETTER_AUTH_URL, CORS_ORIGIN, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_PROD, REQUIRE_EMAIL_VERIFICATION, } from "@/config/constants"; import { addProviderEmail, ProviderId, } from "@/features/user/services/addProviderEmail.service"; import { client } from "@/lib/mongo"; import { userRoleEnum } from "@packages/shared/schemas/user"; import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; import { admin, createAuthMiddleware, mcp, oAuthDiscoveryMetadata, oAuthProtectedResourceMetadata, } from "better-auth/plugins"; import { sendResetPasswordEmail, sendVerificationEmail } from "./resend"; export const auth = betterAuth({ trustedOrigins: [CORS_ORIGIN,"https://chatgpt.com"], database: mongodbAdapter(client), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, errorURL: CORS_ORIGIN, user: { additionalFields: { isOnboardingComplete: { type: "boolean", defaultValue: false, required: false, }, }, }, account: { accountLinking: { enabled: true, allowDifferentEmails: false, updateUserInfoOnLink: true, }, }, plugins: [ admin({ defaultRole: userRoleEnum.USER, adminRoles: [userRoleEnum.ADMIN], }), mcp({ loginPage: `${CORS_ORIGIN}/auth/login`, resource: `${BETTER_AUTH_URL}/api/mcp`, oidcConfig: { loginPage: `${CORS_ORIGIN}/auth/login`, allowDynamicClientRegistration: true, }, }), ], emailAndPassword: { enabled: true, requireEmailVerification: REQUIRE_EMAIL_VERIFICATION, sendResetPassword: async ({ user, url, token }, request) => { await sendResetPasswordEmail(user, url); }, }, emailVerification: { sendOnSignUp: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url, token }, request) => { await sendVerificationEmail(user, url); }, }, session: { expiresIn: 60 * 60 * 24 * 7, // 7 days updateAge: 60 * 60 * 24, // 1 day }, advanced: { defaultCookieAttributes: { sameSite: "none", secure: true, httpOnly: true, }, }, socialProviders: { google: { prompt: "select_account consent", clientId: GOOGLE_CLIENT_ID, clientSecret: GOOGLE_CLIENT_SECRET, scope: [ "openid", "email", "profile", "https://www.googleapis.com/auth/gmail.send", ], accessType: "offline", // Required to get refresh tokens }, // microsoft: { // clientId: MICROSOFT_CLIENT_ID, // clientSecret: MICROSOFT_CLIENT_SECRET, // tenantId: "common", // prompt: "select_account", // }, }, databaseHooks: { account: { create: { after: async (account) => { // skip if providerId is credentials if (account.providerId === "credentials") { return; } // Use service to add provider email await addProviderEmail( account.id, account.providerId as ProviderId, account.accessToken || undefined ); }, }, }, }, hooks: { before: createAuthMiddleware(async (ctx) => { if (ctx.path === "/error") { const { error } = ctx.query as { error?: string }; let redirectUrl = `${CORS_ORIGIN}/?error=${encodeURIComponent( "Authentication Error" )}&error_description=${encodeURIComponent("Failed to authenticate")}`; if (error === "banned") { redirectUrl = `${CORS_ORIGIN}/?error=${encodeURIComponent( "You have been banned" )}&error_description=${encodeURIComponent("Contact support team")}`; } throw ctx.redirect(redirectUrl); } }), }, }); // Use the shared types for consistency export type Session = typeof auth.$Infer.Session.session; export type User = typeof auth.$Infer.Session.user; ``` ### Additional context const app = express(); app.use(morgan("dev")); const allowedOrigins = [ CORS_ORIGIN, "https://chatgpt.com", ]; app.use( cors({ origin(origin, callback) { // Debug: log the incoming origin console.log("Incoming origin:", origin); console.log("Allowed origins:", allowedOrigins); // allow requests with no origin (like from mobile apps, curl, postman) if (!origin) return callback(null, true); if (allowedOrigins.includes(origin)) { callback(null, true); } else { console.log("CORS blocked origin:", origin); callback(new Error("CORS policy does not allow this origin")); } }, credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"], exposedHeaders: ["WWW-Authenticate"], }) ); // make sure cors middleware runs BEFORE JSON parser and routes app.use(express.json()); app.all("/api/auth/*path", toNodeHandler(auth)); // Add JSON parsing middleware for webhook // Note: MCP handler handles its own body parsing, so express.json() won't interfere // Redirect all /.well-known/* requests to /api/auth/.well-known/* for consistency app.all("/.well-known/*path", (req, res) => { const path = req.path.replace("/.well-known", ""); const query = req.url.includes("?") ? req.url.substring(req.url.indexOf("?")) : ""; res.redirect(307, `/api/auth/.well-known${path}${query}`); }); // MCP endpoints must be registered BEFORE body parser // Handler will be initialized in runHealthAndStart before server starts let mcpHandler: any = null; // Register MCP route with body parsing middleware // Use express.json() specifically for MCP route (before global express.json()) app.all("/api/mcp", (req, res, next) => { const requestId = req.body?.id || 'null'; logger.info(`[MCP_ROUTE] Request received: ${req.method} ${req.url} (Request ID: ${requestId})`, "MCP_ROUTE"); logger.info(`[MCP_ROUTE] Body type: ${typeof req.body}, keys: ${req.body ? Object.keys(req.body).join(',') : 'none'}`, "MCP_ROUTE"); if (!mcpHandler) { logger.warn("[MCP_ROUTE] Handler not initialized yet", "MCP_ROUTE"); if (!res.headersSent) { res.json({ jsonrpc: "2.0", id: null, error: { code: -32000, message: "Server Error", data: "MCP handler not initialized. Server is still starting up.", }, }); } return; } // Call the handler - it's already an Express middleware from withMcpAuth logger.info("[MCP_ROUTE] Calling MCP handler", "MCP_ROUTE"); return mcpHandler(req, res, next); });
GiteaMirror added the lockedidentitybug labels 2026-04-13 07:01:08 -05:00
Author
Owner

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

It looks like you're experiencing a CORS error when logging in via an OAuth redirect from ChatGPT to your app's login page. The error indicates that the Access-Control-Allow-Origin header is set to *, which doesn't allow credentialed requests:

Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’.

To resolve this issue, you would need to update your CORS configuration on the server to specify actual origins instead of using the wildcard. Here are a few steps you could take:

  1. Add the Exact Origin: Make sure the CORS settings on your server explicitly list the origin of your application (https://staging.myapp.app) instead of using *. You can modify the allowedOrigins array to include only the necessary origins.

  2. Check Cookie Attributes: Ensure that cookies set for session management are configured correctly with attributes such as SameSite=None and Secure, which are essential for cross-origin requests.

  3. Credentials Flag: Confirm that the credentials option in your CORS configuration is set to true, allowing cookies to be sent with requests.

  4. Update the Better Auth Configuration: Since you are using Better Auth, ensure that the trustedOrigins in your configuration includes your specific client URI (https://staging.myapp.app) and any other necessary origins, such as https://chatgpt.com.

It might also be useful to review the relevant GitHub issue about trusted origins to check for potential updates or workarounds that others have successfully implemented [6], [9].

If these steps don't resolve your issue, I recommend tagging @bekacru or @ping__ for more specific assistance from the Better Auth team.

What has been your experience with configuring CORS in your application so far? Do you have any other questions on this topic?

If you need more help, tag @better-auth-agent in a comment so I can respond.

DiagramDiscordGitHub

Diagram Join Star

<!-- gh-comment-id:3694682112 --> @better-auth-agent[bot] commented on GitHub (Dec 28, 2025): It looks like you're experiencing a CORS error when logging in via an OAuth redirect from ChatGPT to your app's login page. The error indicates that the `Access-Control-Allow-Origin` header is set to `*`, which doesn't allow credentialed requests: ``` Reason: Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*’. ``` To resolve this issue, you would need to update your CORS configuration on the server to specify actual origins instead of using the wildcard. Here are a few steps you could take: 1. **Add the Exact Origin**: Make sure the CORS settings on your server explicitly list the origin of your application (`https://staging.myapp.app`) instead of using `*`. You can modify the `allowedOrigins` array to include only the necessary origins. 2. **Check Cookie Attributes**: Ensure that cookies set for session management are configured correctly with attributes such as `SameSite=None` and `Secure`, which are essential for cross-origin requests. 3. **Credentials Flag**: Confirm that the `credentials` option in your CORS configuration is set to `true`, allowing cookies to be sent with requests. 4. **Update the Better Auth Configuration**: Since you are using Better Auth, ensure that the `trustedOrigins` in your configuration includes your specific client URI (`https://staging.myapp.app`) and any other necessary origins, such as `https://chatgpt.com`. It might also be useful to review the relevant GitHub issue about trusted origins to check for potential updates or workarounds that others have successfully implemented [[6]](https://github.com/better-auth/better-auth/pull/6887), [[9]](https://github.com/better-auth/better-auth/issues/5292). If these steps don't resolve your issue, I recommend tagging @bekacru or @ping__ for more specific assistance from the Better Auth team. What has been your experience with configuring CORS in your application so far? Do you have any other questions on this topic? _If you need more help, tag @better-auth-agent in a comment so I can respond._ <!-- bot:webhook reply v1 --> [Diagram](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) • [Discord](https://discord.gg/fG2XXEuQX3) • [GitHub](https://github.com/Skyvern-AI/Skyvern) [![Diagram](https://img.shields.io/badge/Diagram-2b3137?style=flat-square)](https://repodiagrams.s3.eu-north-1.amazonaws.com/skyvern_ultra_detailed_interactive.html) [![Join](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/fG2XXEuQX3) [![Star](https://img.shields.io/badge/star-181717?logo=github&logoColor=white&style=flat-square)](https://github.com/Skyvern-AI/Skyvern)
Author
Owner

@satish-buildin2 commented on GitHub (Dec 29, 2025):

@Bekacru is there any solution/workaround ?

<!-- gh-comment-id:3695600922 --> @satish-buildin2 commented on GitHub (Dec 29, 2025): @Bekacru is there any solution/workaround ?
Author
Owner

@Rayyan-Balami commented on GitHub (Dec 29, 2025):

When an external application (e.g., “ChatGPT Connect”) redirects users to our OAuth login page, users enter their email & password, and the browser throws a CORS error during the credentialed POST /auth/sign-in/email request.
Despite the error, after refreshing the page the user appears logged in.

This issue occurs during the OIDC code flow (response_type=code) and seems related to wildcard CORS + cookie credentials + redirect (302) behavior.


🔄 Login Flow (Generalized, Safe Example)

External App → Login Page

https://app.example.com/auth/login
  ?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https://external-app.com/oauth/callback
  &state=xyz
  &scope=openid+profile+email+offline_access
  &code_challenge=...
  &code_challenge_method=S256
  &resource=https://api.example.com/mcp

User enters credentials
Client sends request to API:

POST https://api.example.com/auth/sign-in/email
(credentials: include)

The API responds with:

302 Found
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true (or missing)
Set-Cookie: session=...

Browser blocks the response due to CORS.


🚨 Browser Console Error

Access to fetch at 'https://api.example.com/auth/sign-in/email'
from origin 'https://app.example.com' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header must not be '*' 
when the request’s credentials mode is 'include'.

Network Error

POST https://api.example.com/auth/sign-in/email
net::ERR_FAILED 302 (Found)

need a fix for this!

<!-- gh-comment-id:3695823276 --> @Rayyan-Balami commented on GitHub (Dec 29, 2025): When an external application (e.g., “ChatGPT Connect”) redirects users to our OAuth login page, users enter their email & password, and the browser throws a **CORS error** during the credentialed `POST /auth/sign-in/email` request. Despite the error, after refreshing the page the user **appears logged in**. This issue occurs during the OIDC code flow (`response_type=code`) and seems related to **wildcard CORS + cookie credentials + redirect (302)** behavior. --- ## 🔄 Login Flow (Generalized, Safe Example) **External App → Login Page** ``` https://app.example.com/auth/login ?response_type=code &client_id=CLIENT_ID &redirect_uri=https://external-app.com/oauth/callback &state=xyz &scope=openid+profile+email+offline_access &code_challenge=... &code_challenge_method=S256 &resource=https://api.example.com/mcp ``` **User enters credentials** Client sends request to API: ``` POST https://api.example.com/auth/sign-in/email (credentials: include) ``` The API responds with: ``` 302 Found Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true (or missing) Set-Cookie: session=... ``` Browser blocks the response due to CORS. --- ## 🚨 Browser Console Error ``` Access to fetch at 'https://api.example.com/auth/sign-in/email' from origin 'https://app.example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header must not be '*' when the request’s credentials mode is 'include'. ``` ### Network Error ``` POST https://api.example.com/auth/sign-in/email net::ERR_FAILED 302 (Found) ``` need a fix for this!
Author
Owner

@gautamsi commented on GitHub (Jan 4, 2026):

this seems to be problem with authClient.signIn.email which calls the api with POST and follows redirect instead of router.push in nextjs.

This is not CORS problem, this is a bug which incorrectly follows redirect from a fetch call. instead of redirecting users back to application (third party or first party) it follows the redirect in the fetch call and end up trying to access calling app callback endpoint.

<!-- gh-comment-id:3707716342 --> @gautamsi commented on GitHub (Jan 4, 2026): this seems to be problem with `authClient.signIn.email` which calls the api with `POST` and follows redirect instead of `router.push` in nextjs. This is not CORS problem, this is a bug which incorrectly follows redirect from a fetch call. instead of redirecting users back to application (third party or first party) it follows the redirect in the fetch call and end up trying to access calling app callback endpoint.
Author
Owner

@Paola3stefania commented on GitHub (Jan 10, 2026):

This is a known issue with the MCP plugin's redirect handling.

The MCP plugin uses throw ctx.redirect() which returns a 302 redirect. When fetch() follows this redirect to an external URL (like chatgpt.com), browsers block it due to CORS.

The fix exists in the oidcProvider plugin - it detects sec-fetch-mode: cors and returns JSON { redirect: true, url } instead of a 302, which the client handles via window.location.href.

Workaround: Use the oidcProvider plugin instead of mcp:

Change endpoints from /mcp/* to /oauth2/*

Update your OAuth client configuration accordingly

The MCP plugin is being deprecated, so we recommend migrating to oidcProvider.

<!-- gh-comment-id:3732998990 --> @Paola3stefania commented on GitHub (Jan 10, 2026): This is a known issue with the MCP plugin's redirect handling. The MCP plugin uses throw `ctx.redirect() `which returns a 302 redirect. When fetch() follows this redirect to an external URL (like chatgpt.com), browsers block it due to CORS. The fix exists in the **oidcProvider** plugin - it detects sec-fetch-mode: cors and returns JSON { redirect: true, url } instead of a 302, which the client handles via window.location.href. **Workaround: Use the oidcProvider plugin instead of mcp:** Change endpoints from /mcp/* to /oauth2/* Update your OAuth client configuration accordingly The MCP plugin is being deprecated, so we recommend migrating to oidcProvider.
Author
Owner

@gautamsi commented on GitHub (Jan 11, 2026):

I found this behavior in "OAuth Provider" plugin and I was not using this with mcp, I was simply trying to authenticate from webpage. not sure if the latest beta has fixed them, I will check them later next week.

<!-- gh-comment-id:3734049571 --> @gautamsi commented on GitHub (Jan 11, 2026): I found this behavior in "OAuth Provider" plugin and I was not using this with mcp, I was simply trying to authenticate from webpage. not sure if the latest beta has fixed them, I will check them later next week.
Author
Owner

@lebrosoft commented on GitHub (Jan 12, 2026):

Following fix works for me:
better-auth/packages/oauth-provider/src/client.ts/oauthProviderClient/fetchPlugins/hooks/onRequest:

const oauthQuery =
	typeof window !== "undefined"
		? parseSignedQuery(window?.location?.search)
		: undefined;
ctx.body = JSON.stringify({
	...body,
	oauth_query: oauthQuery,
});
// Set accept header to application/json when in OAuth flow
// This ensures the server returns JSON redirect instead of throwing a redirect
// which would cause CORS errors when fetch tries to follow cross-origin redirects
if (oauthQuery) {
	ctx.headers.set("accept", "application/json");
}
<!-- gh-comment-id:3736562587 --> @lebrosoft commented on GitHub (Jan 12, 2026): Following fix works for me: better-auth/packages/oauth-provider/src/client.ts/oauthProviderClient/fetchPlugins/hooks/onRequest: ```javascript const oauthQuery = typeof window !== "undefined" ? parseSignedQuery(window?.location?.search) : undefined; ctx.body = JSON.stringify({ ...body, oauth_query: oauthQuery, }); // Set accept header to application/json when in OAuth flow // This ensures the server returns JSON redirect instead of throwing a redirect // which would cause CORS errors when fetch tries to follow cross-origin redirects if (oauthQuery) { ctx.headers.set("accept", "application/json"); } ```
Author
Owner

@nick938 commented on GitHub (Feb 12, 2026):

I encountered the same issue while using the "OAuth Provider" plugin with Next.js 16 (testing with oidcdebugger.com). I solved it by using Server Actions to ensure cookies are set correctly.

Here is the solution in both Chinese and English.

中文说明 (Chinese)

在使用 Server Functions 时,关键在于调用 auth.api.signInEmail 时必须传入 headers(),这样 Better-Auth 才能正确设置 Set-Cookie。之后在客户端手动跳转即可。

English Explanation

The key is to pass await headers() when calling auth.api.signInEmail inside the Server Action. This ensures Set-Cookie headers are passed correctly to the client. Then, handle the redirect manually on the client side.


1. Server Action (actions.ts)

import { headers } from 'next/headers';
import { auth } from '@/lib/auth'; // path to your auth instance

export async function signInAction(
  email: string,
  password: string,
  rememberMe?: boolean,
  callbackURL?: string
) {
  try {
    // 1. Call Better-Auth Server API / 调用服务端 API
    // ⚠️ KEY: Pass headers so Better-Auth can set the Set-Cookie header
    // ⚠️ 关键:传入 headers,这样 Better-Auth 才能帮你设置 Set-Cookie
    const response = await auth.api.signInEmail({
      body: {
        email,
        password,
        rememberMe,
        callbackURL
      },
      headers: await headers() // 👈 This step is crucial! / 这一步至关重要!
    });

    // 2. Login Successful / 登录成功
    // Note: auth.api does not return a 302 redirect, it returns session data.
    // 注意:auth.api 不会返回 302 跳转,它只返回 session 数据
    return {
      success: true,
      user: response.user
    };
  } catch (error: any) {
    // 3. Error Handling / 处理错误
    return {
      success: false,
      error: error.message || error.body?.message || 'Login failed'
    };
  }
}

2. Client Component (LoginForm.tsx)

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setError(null);

  // Get your target URL (e.g., OIDC Authorize endpoint)
  const targetUrl = getSmartCallbackUrl(); 

  if (!email.trim() || !password) {
    setError('Please enter email and password');
    return;
  }
  
  setIsSubmitting(true);
  
  // 1. Call Server Action / 调用 Server Action
  const result = await signInAction(email, password, rememberMe, '/admin/dashboard');
  
  if (result.success) {
    toast.success('Login success');

    // 2. Core Logic: Manual Redirect after success
    // The Cookie has been set automatically by the Server Action response.
    // Now we just need to send the user to the OIDC Authorize endpoint or callback.
    // 核心逻辑:登录成功后,手动跳转。此时 Cookie 已经由 Server Action 自动种好了。
    
    console.log('Login success, redirecting to:', targetUrl);

    // Use window.location.href to ensure a top-level redirect
    // 使用 window.location.href 确保进行顶层页面跳转
    window.location.href = targetUrl;
  } else {
    toast.error(result.error);
  }
  
  setIsSubmitting(false);
};

<!-- gh-comment-id:3888308929 --> @nick938 commented on GitHub (Feb 12, 2026): I encountered the same issue while using the "OAuth Provider" plugin with Next.js 16 (testing with oidcdebugger.com). I solved it by using **Server Actions** to ensure cookies are set correctly. Here is the solution in both Chinese and English. ### 中文说明 (Chinese) 在使用 Server Functions 时,关键在于调用 `auth.api.signInEmail` 时必须传入 `headers()`,这样 Better-Auth 才能正确设置 `Set-Cookie`。之后在客户端手动跳转即可。 ### English Explanation The key is to pass `await headers()` when calling `auth.api.signInEmail` inside the Server Action. This ensures `Set-Cookie` headers are passed correctly to the client. Then, handle the redirect manually on the client side. --- #### 1. Server Action (`actions.ts`) ```typescript import { headers } from 'next/headers'; import { auth } from '@/lib/auth'; // path to your auth instance export async function signInAction( email: string, password: string, rememberMe?: boolean, callbackURL?: string ) { try { // 1. Call Better-Auth Server API / 调用服务端 API // ⚠️ KEY: Pass headers so Better-Auth can set the Set-Cookie header // ⚠️ 关键:传入 headers,这样 Better-Auth 才能帮你设置 Set-Cookie const response = await auth.api.signInEmail({ body: { email, password, rememberMe, callbackURL }, headers: await headers() // 👈 This step is crucial! / 这一步至关重要! }); // 2. Login Successful / 登录成功 // Note: auth.api does not return a 302 redirect, it returns session data. // 注意:auth.api 不会返回 302 跳转,它只返回 session 数据 return { success: true, user: response.user }; } catch (error: any) { // 3. Error Handling / 处理错误 return { success: false, error: error.message || error.body?.message || 'Login failed' }; } } ``` #### 2. Client Component (`LoginForm.tsx`) ```typescript const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); // Get your target URL (e.g., OIDC Authorize endpoint) const targetUrl = getSmartCallbackUrl(); if (!email.trim() || !password) { setError('Please enter email and password'); return; } setIsSubmitting(true); // 1. Call Server Action / 调用 Server Action const result = await signInAction(email, password, rememberMe, '/admin/dashboard'); if (result.success) { toast.success('Login success'); // 2. Core Logic: Manual Redirect after success // The Cookie has been set automatically by the Server Action response. // Now we just need to send the user to the OIDC Authorize endpoint or callback. // 核心逻辑:登录成功后,手动跳转。此时 Cookie 已经由 Server Action 自动种好了。 console.log('Login success, redirecting to:', targetUrl); // Use window.location.href to ensure a top-level redirect // 使用 window.location.href 确保进行顶层页面跳转 window.location.href = targetUrl; } else { toast.error(result.error); } setIsSubmitting(false); }; ```
Author
Owner

@github-actions[bot] commented on GitHub (Apr 5, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4187977821 --> @github-actions[bot] commented on GitHub (Apr 5, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#10712