Merge pull request #33703 from backstage/feat/auth0-federated-logout
feat(auth): support provider logout redirects, implement Auth0 federated logout
This commit is contained in:
@@ -262,4 +262,55 @@ 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
|
||||
});
|
||||
|
||||
it('should ignore logoutUrl with non-HTTPS protocol', async () => {
|
||||
server.use(
|
||||
rest.post('*', (_req, res, ctx) =>
|
||||
res(ctx.json({ logoutUrl: 'http://evil.com/steal' })),
|
||||
),
|
||||
);
|
||||
|
||||
const connector = new DefaultAuthConnector(defaultOptions);
|
||||
await connector.removeSession();
|
||||
// Should complete normally without redirecting - http:// is rejected
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +194,28 @@ 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) {
|
||||
const url = new URL(body.logoutUrl);
|
||||
if (url.protocol === 'https:' || url.hostname === 'localhost') {
|
||||
window.location.href = body.logoutUrl;
|
||||
return new Promise(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Provider logout redirect is best-effort - the backend session
|
||||
// (refresh token cookie and persisted scopes) is already cleared,
|
||||
// so we degrade gracefully.
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
|
||||
|
||||
Reference in New Issue
Block a user