Skip to content

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

@satish-buildin2

Description

@satish-buildin2

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);
});

Metadata

Metadata

Labels

bugSomething isn't workingidentityOAuth/OIDC provider, MCP, device flow

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions