Skip to content

Commit 1a28543

Browse files
os-zhuangclaude
andauthored
fix(plugin-auth): land OAuth callback errors on the console login page (objectui#2458 item 1) (#2888)
better-auth redirects a failed OAuth callback (expired/replayed state, IdP error) to its default error URL `${baseURL}/error`, which bounces to `/?error=<code>` — and the root→console redirect drops the query string. Net effect: a user who just signed in successfully at the IdP lands on a silent login form (the reported "login silent failure"). Set onAPIError.errorURL to the console login page so the browser lands on `/_console/login?error=<code>`; the console login page (objectui PR, paired) renders that param as a visible error banner. Refs objectstack-ai/objectui#2458 (item 1) Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent da5e686 commit 1a28543

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

packages/plugins/plugin-auth/src/auth-manager.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,30 @@ describe('AuthManager', () => {
886886
});
887887
});
888888

889+
describe('onAPIError.errorURL (objectui#2458 item 1)', () => {
890+
it('points browser-flow errors at the console login page so ?error= is surfaced', async () => {
891+
let capturedConfig: any;
892+
(betterAuth as any).mockImplementation((config: any) => {
893+
capturedConfig = config;
894+
return { handler: vi.fn(), api: {} };
895+
});
896+
897+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
898+
const manager = new AuthManager({
899+
secret: 'test-secret-at-least-32-chars-long',
900+
baseUrl: 'http://localhost:3000',
901+
});
902+
await manager.getAuthInstance();
903+
warnSpy.mockRestore();
904+
905+
// Default error redirect (`${baseURL}/error` → `/?error=…`) loses the
906+
// query on the root→console bounce; the login page renders it instead.
907+
expect(capturedConfig.onAPIError).toEqual({
908+
errorURL: 'http://localhost:3000/_console/login',
909+
});
910+
});
911+
});
912+
889913
describe('emailAndPassword passthrough', () => {
890914
it('should default emailAndPassword to enabled: true', async () => {
891915
let capturedConfig: any;

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,17 @@ export class AuthManager {
11691169
}),
11701170
},
11711171

1172+
// Where better-auth redirects a browser flow that fails server-side —
1173+
// most importantly an OAuth callback error (expired/replayed `state`,
1174+
// IdP error). The default is `${baseURL}/error`, which bounces to
1175+
// `/?error=<code>`; the root→console redirect then DROPS the query, so
1176+
// the user lands on a silent login form right after a successful IdP
1177+
// sign-in (objectui#2458 item 1). Point it at the console login page,
1178+
// which renders `?error=` as an inline banner.
1179+
onAPIError: {
1180+
errorURL: this.getConsolePageUrl('/login'),
1181+
},
1182+
11721183
// Trusted origins for CSRF protection (supports wildcards like "https://*.example.com")
11731184
// Auto-includes origins from OS_CORS_ORIGIN env var so CORS and CSRF stay in sync.
11741185
...(() => {

0 commit comments

Comments
 (0)