diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 192b1b7ac0..02bda40b64 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -262,4 +262,43 @@ describe('DefaultAuthConnector', () => { url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&flow=popup&env=production', }); }); + + it('should not resolve when provider returns a logoutUrl', async () => { + const logoutUrl = + 'https://test.auth0.com/v2/logout?federated&client_id=abc&returnTo=http%3A%2F%2Flocalhost'; + + server.use( + rest.post('*', (_req, res, ctx) => res(ctx.json({ logoutUrl }))), + ); + + const connector = new DefaultAuthConnector(defaultOptions); + + // When a logoutUrl is returned, removeSession redirects the browser and + // returns a never-resolving promise. Race against a short delay to verify + // that it does not resolve. + const result = await Promise.race([ + connector.removeSession().then(() => 'resolved'), + new Promise<'timeout'>(r => setTimeout(() => r('timeout'), 50)), + ]); + + expect(result).toBe('timeout'); + }); + + it('should complete normally when provider returns empty logout response', async () => { + server.use(rest.post('*', (_req, res, ctx) => res(ctx.status(200)))); + + const connector = new DefaultAuthConnector(defaultOptions); + await connector.removeSession(); + // No redirect, no error — the original behavior + }); + + it('should complete normally when response is not JSON', async () => { + server.use( + rest.post('*', (_req, res, ctx) => res(ctx.status(200), ctx.text('OK'))), + ); + + const connector = new DefaultAuthConnector(defaultOptions); + await connector.removeSession(); + // Should complete without error — non-JSON responses are ignored + }); }); diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 3671ddc6e6..32d86b9757 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -194,6 +194,24 @@ export class DefaultAuthConnector error.status = res.status; throw error; } + + // If the auth provider returned a logout URL (e.g. for Auth0 federated + // logout), redirect the browser to clear the provider's session cookies. + try { + const contentType = res.headers.get('content-type'); + if (contentType?.includes('application/json')) { + const body = await res.json(); + if (body.logoutUrl) { + window.location.href = body.logoutUrl; + return new Promise(() => {}); + } + } + } catch { + // Provider logout redirect is best-effort — the Backstage session is + // already cleared, so we degrade gracefully. + } + + return undefined; } private async showPopup(scopes: Set): Promise {