diff --git a/packages/plugins/plugin-auth/src/auth-manager.test.ts b/packages/plugins/plugin-auth/src/auth-manager.test.ts index 31a237930f..5d82d08950 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.test.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.test.ts @@ -886,6 +886,30 @@ describe('AuthManager', () => { }); }); + describe('onAPIError.errorURL (objectui#2458 item 1)', () => { + it('points browser-flow errors at the console login page so ?error= is surfaced', async () => { + let capturedConfig: any; + (betterAuth as any).mockImplementation((config: any) => { + capturedConfig = config; + return { handler: vi.fn(), api: {} }; + }); + + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const manager = new AuthManager({ + secret: 'test-secret-at-least-32-chars-long', + baseUrl: 'http://localhost:3000', + }); + await manager.getAuthInstance(); + warnSpy.mockRestore(); + + // Default error redirect (`${baseURL}/error` → `/?error=…`) loses the + // query on the root→console bounce; the login page renders it instead. + expect(capturedConfig.onAPIError).toEqual({ + errorURL: 'http://localhost:3000/_console/login', + }); + }); + }); + describe('emailAndPassword passthrough', () => { it('should default emailAndPassword to enabled: true', async () => { let capturedConfig: any; diff --git a/packages/plugins/plugin-auth/src/auth-manager.ts b/packages/plugins/plugin-auth/src/auth-manager.ts index eba3583ae0..ca6426b4bb 100644 --- a/packages/plugins/plugin-auth/src/auth-manager.ts +++ b/packages/plugins/plugin-auth/src/auth-manager.ts @@ -1169,6 +1169,17 @@ export class AuthManager { }), }, + // Where better-auth redirects a browser flow that fails server-side — + // most importantly an OAuth callback error (expired/replayed `state`, + // IdP error). The default is `${baseURL}/error`, which bounces to + // `/?error=`; the root→console redirect then DROPS the query, so + // the user lands on a silent login form right after a successful IdP + // sign-in (objectui#2458 item 1). Point it at the console login page, + // which renders `?error=` as an inline banner. + onAPIError: { + errorURL: this.getConsolePageUrl('/login'), + }, + // Trusted origins for CSRF protection (supports wildcards like "https://*.example.com") // Auto-includes origins from OS_CORS_ORIGIN env var so CORS and CSRF stay in sync. ...(() => {