From 0ef5a03fb376a76f9db99df215ebbee8fe0d745b Mon Sep 17 00:00:00 2001 From: Jonathan Roebuck Date: Tue, 31 Mar 2026 14:14:21 +0100 Subject: [PATCH] feat(auth-node): return logoutUrl in logout response when provided by authenticator Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Jonathan Roebuck --- .../oauth/createOAuthRouteHandlers.test.ts | 43 +++++++++++++++++++ .../src/oauth/createOAuthRouteHandlers.ts | 12 +++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts index f2cb27dfa9..c0fece0eaf 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.test.ts @@ -1264,6 +1264,49 @@ describe('createOAuthRouteHandlers', () => { }); }); + it('should return logoutUrl as JSON when authenticator provides one', async () => { + mockAuthenticator.logout.mockResolvedValueOnce({ + logoutUrl: 'https://example.auth0.com/v2/logout?federated', + }); + + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + agent.jar.setCookie( + 'my-provider-refresh-token=my-refresh-token', + '127.0.0.1', + '/my-provider', + ); + + const res = await agent + .post('/my-provider/logout') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({ + logoutUrl: 'https://example.auth0.com/v2/logout?federated', + }); + + // Cookie should still be cleared even when logoutUrl is returned + expect(getRefreshTokenCookie(agent)).toBeUndefined(); + }); + + it('should return empty body when authenticator logout returns void', async () => { + mockAuthenticator.logout.mockResolvedValueOnce(undefined); + + const agent = request.agent( + wrapInApp(createOAuthRouteHandlers(baseConfig)), + ); + + const res = await agent + .post('/my-provider/logout') + .set('X-Requested-With', 'XMLHttpRequest'); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + }); + it('should set error search param and redirect on caught error', async () => { const app = wrapInApp(createOAuthRouteHandlers(baseConfig)); const res = await request(app) diff --git a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts index 0b250eceae..0d268eb052 100644 --- a/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts +++ b/plugins/auth-node/src/oauth/createOAuthRouteHandlers.ts @@ -280,9 +280,13 @@ export function createOAuthRouteHandlers( throw new AuthenticationError('Invalid X-Requested-With header'); } + let logoutResult: void | { logoutUrl?: string }; if (authenticator.logout) { const refreshToken = cookieManager.getRefreshToken(req); - await authenticator.logout({ req, refreshToken }, authenticatorCtx); + logoutResult = await authenticator.logout( + { req, refreshToken }, + authenticatorCtx, + ); } // remove refresh token cookie if it is set @@ -291,7 +295,11 @@ export function createOAuthRouteHandlers( // remove persisted scopes await scopeManager.clear(req); - res.status(200).end(); + if (logoutResult?.logoutUrl) { + res.status(200).json({ logoutUrl: logoutResult.logoutUrl }); + } else { + res.status(200).end(); + } }, async refresh(