feat(auth-node): return logoutUrl in logout response when provided by authenticator

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:
Jonathan Roebuck
2026-03-31 14:14:21 +01:00
parent fec31bdde5
commit 0ef5a03fb3
2 changed files with 53 additions and 2 deletions
@@ -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)
@@ -280,9 +280,13 @@ export function createOAuthRouteHandlers<TProfile>(
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<TProfile>(
// 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(