fix(auth): harden logout redirect with origin validation and protocol check

Add origin allowlist validation in the OAuth logout handler (matching
the existing start/refresh pattern) and validate the logoutUrl protocol
on the frontend before redirecting. Also replace inline type annotation
with the named OAuthAuthenticatorLogoutResult type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Jack Palmer <jackpalmer@spotify.com>
This commit is contained in:
Jack Palmer
2026-04-01 08:55:12 +01:00
committed by Jonathan Roebuck
parent a07f0196e2
commit 3532be4763
2 changed files with 20 additions and 7 deletions
@@ -202,13 +202,17 @@ export class DefaultAuthConnector<AuthSession>
if (contentType?.includes('application/json')) {
const body = await res.json();
if (body.logoutUrl) {
window.location.href = body.logoutUrl;
return new Promise(() => {});
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 Backstage session is
// already cleared, so we degrade gracefully.
// Provider logout redirect is best-effort - the backend session
// (refresh token cookie and persisted scopes) is already cleared,
// so we degrade gracefully.
}
return undefined;
@@ -39,7 +39,11 @@ import {
ProfileTransform,
SignInResolver,
} from '../types';
import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types';
import {
OAuthAuthenticator,
OAuthAuthenticatorLogoutResult,
OAuthAuthenticatorResult,
} from './types';
import { Config, readDurationFromConfig } from '@backstage/config';
import { CookieScopeManager } from './CookieScopeManager';
@@ -280,7 +284,12 @@ export function createOAuthRouteHandlers<TProfile>(
throw new AuthenticationError('Invalid X-Requested-With header');
}
let logoutResult: void | { logoutUrl?: string } = undefined;
const origin = req.get('origin');
if (origin && !isOriginAllowed(origin)) {
throw new NotAllowedError(`Origin '${origin}' is not allowed`);
}
let logoutResult: void | OAuthAuthenticatorLogoutResult = undefined;
if (authenticator.logout) {
const refreshToken = cookieManager.getRefreshToken(req);
logoutResult = await authenticator.logout(
@@ -290,7 +299,7 @@ export function createOAuthRouteHandlers<TProfile>(
}
// remove refresh token cookie if it is set
cookieManager.removeRefreshToken(res, req.get('origin'));
cookieManager.removeRefreshToken(res, origin);
// remove persisted scopes
await scopeManager.clear(req);