feat(core-app-api): redirect to provider logoutUrl on sign-out when available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Jonathan Roebuck <jroebuck@spotify.com>
This commit is contained in:
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +194,24 @@ export class DefaultAuthConnector<AuthSession>
|
||||
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<string>): Promise<AuthSession> {
|
||||
|
||||
Reference in New Issue
Block a user