[GH-ISSUE #8503] The oauth-provider reported a CORS error after login, causing the authentication process to stop #28431

Closed
opened 2026-04-17 19:53:00 -05:00 by GiteaMirror · 4 comments
Owner

Originally created by @HenryOoO on GitHub (Mar 9, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/8503

Is this suited for github?

  • Yes, this is suited for github

To Reproduce

  1. Create the most basic oauth-provider project and set up email login. The login page uses authClient.signIn.email().
  2. Create a most basic general oauth project and log in using authClient.signIn.oauth2
  3. Provider runs on http://localhost:3000, client runs on http://localhost:3100.
  4. After clicking "Log in" on the provider page, a CORS issue was reported```plain
    Access to fetch at 'http://localhost:3100/api/auth/oauth2/callback/example-idp?...'
    (redirected from 'http://localhost:3000/api/auth/sign-in/email')
    from origin 'http://localhost:3000' has been blocked by CORS policy:
    No 'Access-Control-Allow-Origin' header is present on the requested resource.

auth.ts of oauth-provider
``` ts
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { tanstackStartCookies } from 'better-auth/tanstack-start'
import { oauthProvider } from '@better-auth/oauth-provider'
import { db } from '../db'
import { jwt } from 'better-auth/plugins'

export const auth = betterAuth({
  appName: 'PMS',
  database: drizzleAdapter(db, {
    provider: 'sqlite',
  }),
  advanced: {
    cookiePrefix: 'auth-server',
  },
  emailAndPassword: {
    enabled: true,
  },
  disabledPaths: ['/token'],
  plugins: [
    jwt({ disableSettingJwtHeader: true }),
    oauthProvider({
      loginPage: '/sign-in',
      consentPage: '/consent',
      silenceWarnings: { oauthAuthServerConfig: true },
    }),
    tanstackStartCookies(),
  ],
})

auth-client.ts of oauth-provider

import { createAuthClient } from 'better-auth/react'
import { oauthProviderClient } from '@better-auth/oauth-provider/client'
export const authClient = createAuthClient({
  plugins: [oauthProviderClient()],
})

auth client(general oauth) of auth.ts

import { betterAuth } from 'better-auth'
import { tanstackStartCookies } from 'better-auth/tanstack-start'

import { genericOAuth } from 'better-auth/plugins'
export const auth = betterAuth({
    baseURL: process.env.BETTER_AUTH_URL,
    advanced: {
        cookiePrefix: 'auth-client',
    },
    emailAndPassword: {
        enabled: true,
    },
    plugins: [
        genericOAuth({
            config: [
                {
                    providerId: 'example',
                    clientId: process.env.CLIENT_ID!,
                    clientSecret: process.env.CLIENT_SECRET!,
                    discoveryUrl: 'http://localhost:3000/.well-known/openid-configuration',
                    requireIssuerValidation: true,
                    pkce: true,
                },
            ],
        }),
        tanstackStartCookies(),
    ],
})

Current vs. Expected behavior

It is expected that after logging in, a successful callback will be made to the oauth client page. However, it was blocked by a CORS error, seemingly because the email login method of oauth-provider uses fetch followed redirection instead of a browser.

What version of Better Auth are you using?

1.5.3

System info

{
  "system": {
    "platform": "darwin",
    "arch": "arm64",
    "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:45 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8103",
    "release": "25.0.0",
    "cpuCount": 8,
    "cpuModel": "Apple M1",
    "totalMemory": "16.00 GB",
    "freeMemory": "0.14 GB"
  },
  "node": {
    "version": "v24.11.1",
    "env": "development"
  },
  "packageManager": {
    "name": "npm",
    "version": "11.6.2"
  },
  "frameworks": [
    {
      "name": "react",
      "version": "^19.2.0"
    }
  ],
  "databases": [
    {
      "name": "better-sqlite3",
      "version": "^12.5.0"
    },
    {
      "name": "drizzle",
      "version": "^0.45.0"
    }
  ],
  "betterAuth": {
    "version": "^1.5.3",
    "config": {
      "appName": "PMS",
      "advanced": {
        "cookiePrefix": "auth-server"
      },
      "emailAndPassword": {
        "enabled": true
      },
      "disabledPaths": [
        "/token"
      ],
      "plugins": [
        {
          "name": "jwt",
          "config": {
            "id": "jwt",
            "options": {
              "disableSettingJwtHeader": true
            },
            "endpoints": {},
            "hooks": {
              "after": [
                {}
              ]
            },
            "schema": {
              "jwks": {
                "fields": {
                  "publicKey": {
                    "type": "string",
                    "required": true
                  },
                  "privateKey": {
                    "type": "string",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date",
                    "required": true
                  },
                  "expiresAt": {
                    "type": "date",
                    "required": false
                  }
                }
              }
            }
          }
        },
        {
          "name": "oauth-provider",
          "config": {
            "id": "oauth-provider",
            "options": {
              "codeExpiresIn": 600,
              "accessTokenExpiresIn": 3600,
              "m2mAccessTokenExpiresIn": 3600,
              "refreshTokenExpiresIn": 2592000,
              "allowUnauthenticatedClientRegistration": false,
              "allowDynamicClientRegistration": false,
              "disableJwtPlugin": false,
              "storeClientSecret": "[REDACTED]",
              "storeTokens": "hashed",
              "grantTypes": [
                "authorization_code",
                "client_credentials",
                "refresh_token"
              ],
              "loginPage": "/sign-in",
              "consentPage": "/consent",
              "silenceWarnings": {
                "oauthAuthServerConfig": true
              },
              "scopes": [
                "openid",
                "profile",
                "email",
                "offline_access"
              ],
              "claims": [
                "sub",
                "iss",
                "aud",
                "exp",
                "iat",
                "sid",
                "scope",
                "azp",
                "email",
                "email_verified",
                "name",
                "picture",
                "family_name",
                "given_name"
              ]
            },
            "hooks": {
              "before": [
                {}
              ],
              "after": [
                {}
              ]
            },
            "endpoints": {},
            "schema": {
              "oauthClient": {
                "modelName": "oauthClient",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "unique": true,
                    "required": true
                  },
                  "clientSecret": {
                    "type": "string",
                    "required": false
                  },
                  "disabled": {
                    "type": "boolean",
                    "defaultValue": false,
                    "required": false
                  },
                  "skipConsent": {
                    "type": "boolean",
                    "required": false
                  },
                  "enableEndSession": {
                    "type": "boolean",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": false
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "createdAt": {
                    "type": "date",
                    "required": false
                  },
                  "updatedAt": {
                    "type": "date",
                    "required": false
                  },
                  "name": {
                    "type": "string",
                    "required": false
                  },
                  "uri": {
                    "type": "string",
                    "required": false
                  },
                  "icon": {
                    "type": "string",
                    "required": false
                  },
                  "contacts": {
                    "type": "string[]",
                    "required": false
                  },
                  "tos": {
                    "type": "string",
                    "required": false
                  },
                  "policy": {
                    "type": "string",
                    "required": false
                  },
                  "softwareId": {
                    "type": "string",
                    "required": false
                  },
                  "softwareVersion": {
                    "type": "string",
                    "required": false
                  },
                  "softwareStatement": {
                    "type": "string",
                    "required": false
                  },
                  "redirectUris": {
                    "type": "string[]",
                    "required": true
                  },
                  "postLogoutRedirectUris": {
                    "type": "string[]",
                    "required": false
                  },
                  "tokenEndpointAuthMethod": {
                    "type": "string",
                    "required": false
                  },
                  "grantTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "responseTypes": {
                    "type": "string[]",
                    "required": false
                  },
                  "public": {
                    "type": "boolean",
                    "required": false
                  },
                  "type": {
                    "type": "string",
                    "required": false
                  },
                  "requirePKCE": {
                    "type": "boolean",
                    "required": false
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "metadata": {
                    "type": "json",
                    "required": false
                  }
                }
              },
              "oauthRefreshToken": {
                "fields": {
                  "token": {
                    "type": "string",
                    "required": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "revoked": {
                    "type": "date",
                    "required": false
                  },
                  "authTime": {
                    "type": "date",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthAccessToken": {
                "modelName": "oauthAccessToken",
                "fields": {
                  "token": {
                    "type": "string",
                    "unique": true
                  },
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "sessionId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "session",
                      "field": "id",
                      "onDelete": "set null"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "refreshId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "oauthRefreshToken",
                      "field": "id"
                    }
                  },
                  "expiresAt": {
                    "type": "date"
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  }
                }
              },
              "oauthConsent": {
                "modelName": "oauthConsent",
                "fields": {
                  "clientId": {
                    "type": "string",
                    "required": true,
                    "references": {
                      "model": "oauthClient",
                      "field": "clientId"
                    }
                  },
                  "userId": {
                    "type": "string",
                    "required": false,
                    "references": {
                      "model": "user",
                      "field": "id"
                    }
                  },
                  "referenceId": {
                    "type": "string",
                    "required": false
                  },
                  "scopes": {
                    "type": "string[]",
                    "required": true
                  },
                  "createdAt": {
                    "type": "date"
                  },
                  "updatedAt": {
                    "type": "date"
                  }
                }
              }
            },
            "rateLimit": [
              {
                "window": 60,
                "max": 20
              },
              {
                "window": 60,
                "max": 30
              },
              {
                "window": 60,
                "max": 100
              },
              {
                "window": 60,
                "max": 30
              },
              {
                "window": 60,
                "max": 5
              },
              {
                "window": 60,
                "max": 60
              }
            ]
          }
        },
        {
          "name": "tanstack-start-cookies",
          "config": {
            "id": "tanstack-start-cookies",
            "hooks": {
              "after": [
                {}
              ]
            }
          }
        }
      ]
    }
  }
}

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

Backend, Client

Auth config (if applicable)

import { betterAuth } from "better-auth"
export const auth = betterAuth({
  emailAndPassword: {  
    enabled: true
  },
});

Additional context

Itested my reproduction against the latest release.
I searched for multiple issues but found no solutions. Even after updating to the latest version, this problem still exists.

This seems very similar to #7558, #8088 and possibly related to PR #7280.

Originally created by @HenryOoO on GitHub (Mar 9, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/8503 ### Is this suited for github? - [x] Yes, this is suited for github ### To Reproduce 1. Create the most basic oauth-provider project and set up email login. The login page uses authClient.signIn.email(). 2. Create a most basic general oauth project and log in using authClient.signIn.oauth2 3. Provider runs on `http://localhost:3000`, client runs on `http://localhost:3100`. 4. After clicking "Log in" on the provider page, a CORS issue was reported```plain Access to fetch at 'http://localhost:3100/api/auth/oauth2/callback/example-idp?...' (redirected from 'http://localhost:3000/api/auth/sign-in/email') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` auth.ts of oauth-provider ``` ts import { betterAuth } from 'better-auth' import { drizzleAdapter } from 'better-auth/adapters/drizzle' import { tanstackStartCookies } from 'better-auth/tanstack-start' import { oauthProvider } from '@better-auth/oauth-provider' import { db } from '../db' import { jwt } from 'better-auth/plugins' export const auth = betterAuth({ appName: 'PMS', database: drizzleAdapter(db, { provider: 'sqlite', }), advanced: { cookiePrefix: 'auth-server', }, emailAndPassword: { enabled: true, }, disabledPaths: ['/token'], plugins: [ jwt({ disableSettingJwtHeader: true }), oauthProvider({ loginPage: '/sign-in', consentPage: '/consent', silenceWarnings: { oauthAuthServerConfig: true }, }), tanstackStartCookies(), ], }) ``` auth-client.ts of oauth-provider ```ts import { createAuthClient } from 'better-auth/react' import { oauthProviderClient } from '@better-auth/oauth-provider/client' export const authClient = createAuthClient({ plugins: [oauthProviderClient()], }) ``` auth client(general oauth) of auth.ts ```ts import { betterAuth } from 'better-auth' import { tanstackStartCookies } from 'better-auth/tanstack-start' import { genericOAuth } from 'better-auth/plugins' export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL, advanced: { cookiePrefix: 'auth-client', }, emailAndPassword: { enabled: true, }, plugins: [ genericOAuth({ config: [ { providerId: 'example', clientId: process.env.CLIENT_ID!, clientSecret: process.env.CLIENT_SECRET!, discoveryUrl: 'http://localhost:3000/.well-known/openid-configuration', requireIssuerValidation: true, pkce: true, }, ], }), tanstackStartCookies(), ], }) ``` ### Current vs. Expected behavior It is expected that after logging in, a successful callback will be made to the oauth client page. However, it was blocked by a CORS error, seemingly because the email login method of oauth-provider uses fetch followed redirection instead of a browser. ### What version of Better Auth are you using? 1.5.3 ### System info ```bash { "system": { "platform": "darwin", "arch": "arm64", "version": "Darwin Kernel Version 25.0.0: Mon Aug 25 21:17:45 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T8103", "release": "25.0.0", "cpuCount": 8, "cpuModel": "Apple M1", "totalMemory": "16.00 GB", "freeMemory": "0.14 GB" }, "node": { "version": "v24.11.1", "env": "development" }, "packageManager": { "name": "npm", "version": "11.6.2" }, "frameworks": [ { "name": "react", "version": "^19.2.0" } ], "databases": [ { "name": "better-sqlite3", "version": "^12.5.0" }, { "name": "drizzle", "version": "^0.45.0" } ], "betterAuth": { "version": "^1.5.3", "config": { "appName": "PMS", "advanced": { "cookiePrefix": "auth-server" }, "emailAndPassword": { "enabled": true }, "disabledPaths": [ "/token" ], "plugins": [ { "name": "jwt", "config": { "id": "jwt", "options": { "disableSettingJwtHeader": true }, "endpoints": {}, "hooks": { "after": [ {} ] }, "schema": { "jwks": { "fields": { "publicKey": { "type": "string", "required": true }, "privateKey": { "type": "string", "required": true }, "createdAt": { "type": "date", "required": true }, "expiresAt": { "type": "date", "required": false } } } } } }, { "name": "oauth-provider", "config": { "id": "oauth-provider", "options": { "codeExpiresIn": 600, "accessTokenExpiresIn": 3600, "m2mAccessTokenExpiresIn": 3600, "refreshTokenExpiresIn": 2592000, "allowUnauthenticatedClientRegistration": false, "allowDynamicClientRegistration": false, "disableJwtPlugin": false, "storeClientSecret": "[REDACTED]", "storeTokens": "hashed", "grantTypes": [ "authorization_code", "client_credentials", "refresh_token" ], "loginPage": "/sign-in", "consentPage": "/consent", "silenceWarnings": { "oauthAuthServerConfig": true }, "scopes": [ "openid", "profile", "email", "offline_access" ], "claims": [ "sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp", "email", "email_verified", "name", "picture", "family_name", "given_name" ] }, "hooks": { "before": [ {} ], "after": [ {} ] }, "endpoints": {}, "schema": { "oauthClient": { "modelName": "oauthClient", "fields": { "clientId": { "type": "string", "unique": true, "required": true }, "clientSecret": { "type": "string", "required": false }, "disabled": { "type": "boolean", "defaultValue": false, "required": false }, "skipConsent": { "type": "boolean", "required": false }, "enableEndSession": { "type": "boolean", "required": false }, "scopes": { "type": "string[]", "required": false }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "createdAt": { "type": "date", "required": false }, "updatedAt": { "type": "date", "required": false }, "name": { "type": "string", "required": false }, "uri": { "type": "string", "required": false }, "icon": { "type": "string", "required": false }, "contacts": { "type": "string[]", "required": false }, "tos": { "type": "string", "required": false }, "policy": { "type": "string", "required": false }, "softwareId": { "type": "string", "required": false }, "softwareVersion": { "type": "string", "required": false }, "softwareStatement": { "type": "string", "required": false }, "redirectUris": { "type": "string[]", "required": true }, "postLogoutRedirectUris": { "type": "string[]", "required": false }, "tokenEndpointAuthMethod": { "type": "string", "required": false }, "grantTypes": { "type": "string[]", "required": false }, "responseTypes": { "type": "string[]", "required": false }, "public": { "type": "boolean", "required": false }, "type": { "type": "string", "required": false }, "requirePKCE": { "type": "boolean", "required": false }, "referenceId": { "type": "string", "required": false }, "metadata": { "type": "json", "required": false } } }, "oauthRefreshToken": { "fields": { "token": { "type": "string", "required": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": true, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "revoked": { "type": "date", "required": false }, "authTime": { "type": "date", "required": false }, "scopes": { "type": "string[]", "required": true } } }, "oauthAccessToken": { "modelName": "oauthAccessToken", "fields": { "token": { "type": "string", "unique": true }, "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "sessionId": { "type": "string", "required": false, "references": { "model": "session", "field": "id", "onDelete": "set null" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "refreshId": { "type": "string", "required": false, "references": { "model": "oauthRefreshToken", "field": "id" } }, "expiresAt": { "type": "date" }, "createdAt": { "type": "date" }, "scopes": { "type": "string[]", "required": true } } }, "oauthConsent": { "modelName": "oauthConsent", "fields": { "clientId": { "type": "string", "required": true, "references": { "model": "oauthClient", "field": "clientId" } }, "userId": { "type": "string", "required": false, "references": { "model": "user", "field": "id" } }, "referenceId": { "type": "string", "required": false }, "scopes": { "type": "string[]", "required": true }, "createdAt": { "type": "date" }, "updatedAt": { "type": "date" } } } }, "rateLimit": [ { "window": 60, "max": 20 }, { "window": 60, "max": 30 }, { "window": 60, "max": 100 }, { "window": 60, "max": 30 }, { "window": 60, "max": 5 }, { "window": 60, "max": 60 } ] } }, { "name": "tanstack-start-cookies", "config": { "id": "tanstack-start-cookies", "hooks": { "after": [ {} ] } } } ] } } } ``` ### Which area(s) are affected? (Select all that apply) Backend, Client ### Auth config (if applicable) ```typescript import { betterAuth } from "better-auth" export const auth = betterAuth({ emailAndPassword: { enabled: true }, }); ``` ### Additional context Itested my reproduction against the latest release. I searched for multiple issues but found no solutions. Even after updating to the latest version, this problem still exists. This seems very similar to #7558, #8088 and possibly related to PR #7280.
GiteaMirror added the lockedbug labels 2026-04-17 19:53:00 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Mar 9, 2026):

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

Your suspicion is correct — the CORS error happens because authClient.signIn.email() uses fetch, and when the oauth-provider plugin responds with an HTTP 302 redirect instead of JSON, browsers block it due to CORS restrictions on credentialed cross-origin redirects.

This is a known issue with the oauth-provider plugin. The root cause is in the redirect handling logic — when the Accept header doesn't include application/json, the server returns a 302 redirect that fetch() cannot follow cross-origin.

Quick fix: Handle the redirect manually on the client side:

const { data, error } = await authClient.signIn.email({
  email,
  password,
  fetchOptions: {
    onSuccess: async (context) => {
      if (context.data?.redirect && context.data?.url) {
        window.location.href = context.data.url;
      }
    }
  }
});

Also ensure your CORS configuration uses specific origins (not wildcards) and configure trustedOrigins in your Better Auth config:

trustedOrigins: [
  "https://your-oauth-client.com",
  "https://your-provider.com"
]

Note: The authentication actually succeeds despite the error — if you refresh the page, you'll likely see the user is logged in. The issue is just with the redirect flow completing properly.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4020540468 --> @dosubot[bot] commented on GitHub (Mar 9, 2026): <!-- Greeting --> Hi @HenryOoO! I'm [Dosu](https://go.dosu.dev/dosubot) and I’m helping the better-auth team. <!-- Answer --> Your suspicion is correct — the CORS error happens because `authClient.signIn.email()` uses fetch, and when the oauth-provider plugin responds with an HTTP 302 redirect instead of JSON, browsers block it due to CORS restrictions on credentialed cross-origin redirects. This is a [known issue](https://github.com/better-auth/better-auth/issues/7041) with the oauth-provider plugin. The root cause is in the [redirect handling logic](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/oauth-provider/src/authorize.ts#L42-L52) — when the `Accept` header doesn't include `application/json`, the server returns a 302 redirect that fetch() cannot follow cross-origin. **Quick fix**: Handle the redirect manually on the client side: ```typescript const { data, error } = await authClient.signIn.email({ email, password, fetchOptions: { onSuccess: async (context) => { if (context.data?.redirect && context.data?.url) { window.location.href = context.data.url; } } } }); ``` **Also ensure** your CORS configuration uses specific origins (not wildcards) and configure `trustedOrigins` in your Better Auth config: ```typescript trustedOrigins: [ "https://your-oauth-client.com", "https://your-provider.com" ] ``` Note: The authentication actually succeeds despite the error — if you refresh the page, you'll likely see the user is logged in. The issue is just with the redirect flow completing properly. <!-- 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/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/d02ef1e3-ee3f-45d0-8251-a6412988201e?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Your%20suspicion%20is%20correct%20%E2%80%94%20the%20CORS%20error%20happens%20because%20%60authClient.signIn.email%28%29%60%20uses%20fetch%2C%20and%20when%20the%20oauth-provider%20plugin%20responds%20with%20an%20HTTP%20302%20redirect%20instead%20of%20JSON%2C%20browsers%20block%20it%20due%20to%20CORS%20restrictions%20on%20credentialed%20cross-origin%20redirects.%0A%0AThis%20is%20a%20%5Bknown%20issue%5D%28https%3A//github.com/better-auth/better-auth/issues/7041%29%20with%20the%20oauth-provider%20plugin.%20The%20root%20cause%20is%20in%20the%20%5Bredirect%20handling%20logic%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/oauth-provider/src/authorize.ts%23L42-L52%29%20%E2%80%94%20when%20the%20%60Accept%60%20header%20doesn%27t%20include%20%60application/json%60%2C%20the%20server%20returns%20a%20302%20redirect%20that%20fetch%28%29%20cannot%20follow%20cross-origin.%0A%0A%2A%2AQuick%20fix%2A%2A%3A%20Handle%20the%20redirect%20manually%20on%20the%20client%20side%3A%0A%0A%60%60%60typescript%0Aconst%20%7B%20data%2C%20error%20%7D%20%3D%20await%20authClient.signIn.email%28%7B%0A%20%20email%2C%0A%20%20password%2C%0A%20%20fetchOptions%3A%20%7B%0A%20%20%20%20onSuccess%3A%20async%20%28context%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20if%20%28context.data%3F.redirect%20%26%26%20context.data%3F.url%29%20%7B%0A%20%20%20%20%20%20%20%20window.location.href%20%3D%20context.data.url%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%29%3B%0A%60%60%60%0A%0A%2A%2AAlso%20ensure%2A%2A%20your%20CORS%20configuration%20uses%20specific%20origins%20%28not%20wildcards%29%20and%20configure%20%60trustedOrigins%60%20in%20your%20Better%20Auth%20config%3A%0A%0A%60%60%60typescript%0AtrustedOrigins%3A%20%5B%0A%20%20%22https%3A//your-oauth-client.com%22%2C%0A%20%20%22https%3A//your-provider.com%22%0A%5D%0A%60%60%60%0A%0ANote%3A%20The%20authentication%20actually%20succeeds%20despite%20the%20error%20%E2%80%94%20if%20you%20refresh%20the%20page%2C%20you%27ll%20likely%20see%20the%20user%20is%20logged%20in.%20The%20issue%20is%20just%20with%20the%20redirect%20flow%20completing%20properly.)&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/8503)
Author
Owner

@HenryOoO commented on GitHub (Mar 9, 2026):

@dosu
I have tried both of these methods, but they were ineffective.
The first method simply cannot reach the onSuccess hook of fetch due to a cors error.
Although the second method was set up, it had no effect at all.

<!-- gh-comment-id:4020559794 --> @HenryOoO commented on GitHub (Mar 9, 2026): @dosu I have tried both of these methods, but they were ineffective. The first method simply cannot reach the onSuccess hook of fetch due to a cors error. Although the second method was set up, it had no effect at all.
Author
Owner

@dosubot[bot] commented on GitHub (Mar 9, 2026):

Thanks for the update. The issue is that you need to set the Accept header explicitly to force the server to return JSON instead of an HTTP redirect. Without this, the 302 redirect triggers CORS before your callback can run.

Try this approach:

const { data, error } = await authClient.signIn.email({
  email,
  password,
  fetchOptions: {
    headers: {
      'accept': 'application/json'  // Forces JSON response instead of 302 redirect
    },
    onSuccess: async (context) => {
      if (context.data?.redirect && context.data?.url) {
        window.location.href = context.data.url;
      }
    }
  }
});

The oauth-provider checks the Accept header and returns { redirect: true, url: "..." } instead of a 302 redirect when application/json is present. This avoids the CORS issue entirely because there's no cross-origin redirect to follow.

Also note that trustedOrigins alone doesn't set CORS headers — it only validates redirect URLs. You still need proper CORS middleware at your server level if you're making cross-origin requests.

Let me know if the Accept header approach works for your setup!

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:4020572977 --> @dosubot[bot] commented on GitHub (Mar 9, 2026): <!-- Answer --> Thanks for the update. The issue is that you need to **set the `Accept` header explicitly** to force the server to return JSON instead of an HTTP redirect. Without this, the 302 redirect triggers CORS before your callback can run. Try this approach: ```typescript const { data, error } = await authClient.signIn.email({ email, password, fetchOptions: { headers: { 'accept': 'application/json' // Forces JSON response instead of 302 redirect }, onSuccess: async (context) => { if (context.data?.redirect && context.data?.url) { window.location.href = context.data.url; } } } }); ``` The [oauth-provider checks the `Accept` header](https://github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/oauth-provider/src/authorize.ts#L42-L52) and returns `{ redirect: true, url: "..." }` instead of a 302 redirect when `application/json` is present. This avoids the CORS issue entirely because there's no cross-origin redirect to follow. Also note that `trustedOrigins` alone [doesn't set CORS headers](https://github.com/better-auth/better-auth/issues/7041) — it only validates redirect URLs. You still need proper CORS middleware at your server level if you're making cross-origin requests. Let me know if the Accept header approach works for your 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/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/ac0ea5f6-b743-412d-b340-a5f53c81a1b7?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=Thanks%20for%20the%20update.%20The%20issue%20is%20that%20you%20need%20to%20%2A%2Aset%20the%20%60Accept%60%20header%20explicitly%2A%2A%20to%20force%20the%20server%20to%20return%20JSON%20instead%20of%20an%20HTTP%20redirect.%20Without%20this%2C%20the%20302%20redirect%20triggers%20CORS%20before%20your%20callback%20can%20run.%0A%0ATry%20this%20approach%3A%0A%0A%60%60%60typescript%0Aconst%20%7B%20data%2C%20error%20%7D%20%3D%20await%20authClient.signIn.email%28%7B%0A%20%20email%2C%0A%20%20password%2C%0A%20%20fetchOptions%3A%20%7B%0A%20%20%20%20headers%3A%20%7B%0A%20%20%20%20%20%20%27accept%27%3A%20%27application/json%27%20%20//%20Forces%20JSON%20response%20instead%20of%20302%20redirect%0A%20%20%20%20%7D%2C%0A%20%20%20%20onSuccess%3A%20async%20%28context%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20if%20%28context.data%3F.redirect%20%26%26%20context.data%3F.url%29%20%7B%0A%20%20%20%20%20%20%20%20window.location.href%20%3D%20context.data.url%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%29%3B%0A%60%60%60%0A%0AThe%20%5Boauth-provider%20checks%20the%20%60Accept%60%20header%5D%28https%3A//github.com/better-auth/better-auth/blob/8e0e53ed9b8918f43dd8e60194580d1357f9e0fb/packages/oauth-provider/src/authorize.ts%23L42-L52%29%20and%20returns%20%60%7B%20redirect%3A%20true%2C%20url%3A%20%22...%22%20%7D%60%20instead%20of%20a%20302%20redirect%20when%20%60application/json%60%20is%20present.%20This%20avoids%20the%20CORS%20issue%20entirely%20because%20there%27s%20no%20cross-origin%20redirect%20to%20follow.%0A%0AAlso%20note%20that%20%60trustedOrigins%60%20alone%20%5Bdoesn%27t%20set%20CORS%20headers%5D%28https%3A//github.com/better-auth/better-auth/issues/7041%29%20%E2%80%94%20it%20only%20validates%20redirect%20URLs.%20You%20still%20need%20proper%20CORS%20middleware%20at%20your%20server%20level%20if%20you%27re%20making%20cross-origin%20requests.%0A%0ALet%20me%20know%20if%20the%20Accept%20header%20approach%20works%20for%20your%20setup%21)&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/8503)
Author
Owner

@GautamBytes commented on GitHub (Mar 9, 2026):

Looking into it!

<!-- gh-comment-id:4024941332 --> @GautamBytes commented on GitHub (Mar 9, 2026): Looking into it!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#28431