-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Error: Access-Control-Allow-Origin * when i was redirected from chatgpt to login screen of my app #7041
Description
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);
});