correcting keycloak logout URL

This commit is contained in:
DarioGii 2025-12-10 22:49:41 +00:00 committed by Dario Ghunney Ware
parent 27381286c9
commit a60977931b
2 changed files with 472 additions and 34 deletions

View File

@ -10,6 +10,7 @@ import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
@ -41,6 +42,7 @@ import stirling.software.proprietary.security.service.JwtServiceInterface;
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
public static final String LOGOUT_PATH = "/login?logout=true";
public static final String KEYCLOAK_LOGOUT_PATH = "/protocol/openid-connect/logout";
private final ApplicationProperties.Security securityProperties;
@ -150,48 +152,57 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak" -> {
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
if (registrationId.equalsIgnoreCase("keycloak")) {
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
boolean isKeycloak = !keycloak.getIssuer().isBlank();
boolean isCustomOAuth = !oauth.getIssuer().isBlank();
boolean isKeycloak = !keycloak.getIssuer().isBlank();
boolean isCustomOAuth = !oauth.getIssuer().isBlank();
String logoutUrl = redirectUrl;
String logoutUrl = redirectUrl;
if (isKeycloak) {
logoutUrl = keycloak.getIssuer();
} else if (isCustomOAuth) {
logoutUrl = oauth.getIssuer();
}
if (isKeycloak || isCustomOAuth) {
logoutUrl +=
"/protocol/openid-connect/logout"
+ "?client_id="
+ keycloak.getClientId()
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirectUrl);
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
if (isKeycloak) {
logoutUrl = keycloak.getIssuer();
} else if (isCustomOAuth) {
logoutUrl = oauth.getIssuer();
}
if (isKeycloak || isCustomOAuth) {
StringBuilder logoutUrlBuilder = new StringBuilder(logoutUrl);
logoutUrlBuilder.append(KEYCLOAK_LOGOUT_PATH);
// Extract id_token_hint if available (OIDC)
// - Identifies the specific user session to terminate
// - Enables proper back-channel logout notifications
// - Skips the logout confirmation screen in Keycloak
Object principal = oAuthToken.getPrincipal();
if (principal instanceof OidcUser oidcUser) {
String idToken = oidcUser.getIdToken().getTokenValue();
logoutUrlBuilder.append("?id_token_hint=").append(idToken);
logoutUrlBuilder
.append("&post_logout_redirect_uri=")
.append(response.encodeRedirectURL(redirectUrl));
// client_id is optional when id_token_hint is present, but included for
// compatibility
logoutUrlBuilder.append("&client_id=").append(keycloak.getClientId());
} else {
log.info(
"No redirect URL for {} available. Redirecting to default logout URL:"
+ " {}",
registrationId,
logoutUrl);
// Fallback to client_id
logoutUrlBuilder.append("?client_id=").append(keycloak.getClientId());
logoutUrlBuilder
.append("&post_logout_redirect_uri=")
.append(response.encodeRedirectURL(redirectUrl));
}
response.sendRedirect(logoutUrl);
}
case "github", "google" -> {
logoutUrl = logoutUrlBuilder.toString();
log.debug("Keycloak logout URL: {}", logoutUrl);
} else {
log.info(
"No redirect URL for {} available. Redirecting to default logout URL: {}",
"No Keycloak logout available for {}. Redirecting to default logout URL: {}",
registrationId,
redirectUrl);
response.sendRedirect(redirectUrl);
}
default -> {
log.info("Redirecting to default logout URL: {}", redirectUrl);
response.sendRedirect(redirectUrl);
logoutUrl);
}
response.sendRedirect(logoutUrl);
} else {
log.info("Redirecting to default logout URL: {}", redirectUrl);
response.sendRedirect(redirectUrl);
}
}

View File

@ -1,23 +1,38 @@
package stirling.software.proprietary.security;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.common.configuration.AppConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.oauth2.KeycloakProvider;
import stirling.software.proprietary.security.saml2.CertificateUtils;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.service.JwtServiceInterface;
@ExtendWith(MockitoExtension.class)
@ -287,4 +302,416 @@ class CustomLogoutSuccessHandlerTest {
verify(response).sendRedirect(url + "/login?errorOAuth=" + error);
}
@Test
void testKeycloakLogoutWithOidcUser_IncludesIdTokenHint() throws IOException {
// Test that Keycloak logout with OidcUser includes id_token_hint parameter
String idTokenValue = "test.id.token";
String issuerUrl = "https://keycloak.example.com/realms/test";
String clientId = "stirling-pdf";
String redirectUrl = "http://localhost:8080/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
ApplicationProperties.Security.OAUTH2.Client client =
mock(ApplicationProperties.Security.OAUTH2.Client.class);
KeycloakProvider keycloakProvider = mock(KeycloakProvider.class);
// Create OidcUser with id token
OidcIdToken idToken =
new OidcIdToken(
idTokenValue,
Instant.now(),
Instant.now().plusSeconds(3600),
java.util.Map.of("sub", "user123"));
OidcUser oidcUser = mock(OidcUser.class);
when(oidcUser.getIdToken()).thenReturn(idToken);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(anyString())).thenReturn(redirectUrl);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(oauth.getClient()).thenReturn(client);
when(oauth.getIssuer()).thenReturn(""); // Empty custom issuer
when(client.getKeycloak()).thenReturn(keycloakProvider);
when(keycloakProvider.getIssuer()).thenReturn(issuerUrl);
when(keycloakProvider.getClientId()).thenReturn(clientId);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("keycloak");
when(authentication.getPrincipal()).thenReturn(oidcUser);
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify the logout URL contains id_token_hint
verify(response)
.sendRedirect(
contains(issuerUrl + "/protocol/openid-connect/logout?id_token_hint="));
verify(response).sendRedirect(contains("id_token_hint=" + idTokenValue));
verify(response).sendRedirect(contains("post_logout_redirect_uri="));
verify(response).sendRedirect(contains("client_id=" + clientId));
}
@Test
void testKeycloakLogoutWithoutOidcUser_FallsBackToClientId() throws IOException {
// Test that Keycloak logout without OidcUser falls back to client_id only
String issuerUrl = "https://keycloak.example.com/realms/test";
String clientId = "stirling-pdf";
String redirectUrl = "http://localhost:8080/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
ApplicationProperties.Security.OAUTH2.Client client =
mock(ApplicationProperties.Security.OAUTH2.Client.class);
KeycloakProvider keycloakProvider = mock(KeycloakProvider.class);
// Create non-OIDC OAuth2User (no id token available)
OAuth2User oauth2User = mock(OAuth2User.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(anyString())).thenReturn(redirectUrl);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(oauth.getClient()).thenReturn(client);
when(oauth.getIssuer()).thenReturn(""); // Empty custom issuer
when(client.getKeycloak()).thenReturn(keycloakProvider);
when(keycloakProvider.getIssuer()).thenReturn(issuerUrl);
when(keycloakProvider.getClientId()).thenReturn(clientId);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("keycloak");
when(authentication.getPrincipal()).thenReturn(oauth2User);
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify the logout URL uses client_id without id_token_hint
verify(response)
.sendRedirect(
issuerUrl
+ "/protocol/openid-connect/logout?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ redirectUrl);
}
@Test
void testKeycloakLogoutWithCustomOAuth_UsesCustomIssuer() throws IOException {
// Test that custom OAuth provider uses custom issuer URL
String customIssuerUrl = "https://custom-oauth.example.com";
String clientId = "stirling-pdf";
String redirectUrl = "http://localhost:8080/login?logout=true";
String idTokenValue = "custom.id.token";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
ApplicationProperties.Security.OAUTH2.Client client =
mock(ApplicationProperties.Security.OAUTH2.Client.class);
KeycloakProvider keycloakProvider = mock(KeycloakProvider.class);
// Create OidcUser with id token
OidcIdToken idToken =
new OidcIdToken(
idTokenValue,
Instant.now(),
Instant.now().plusSeconds(3600),
java.util.Map.of("sub", "user123"));
OidcUser oidcUser = mock(OidcUser.class);
when(oidcUser.getIdToken()).thenReturn(idToken);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(response.encodeRedirectURL(anyString())).thenReturn(redirectUrl);
when(securityProperties.getOauth2()).thenReturn(oauth);
when(oauth.getClient()).thenReturn(client);
when(client.getKeycloak()).thenReturn(keycloakProvider);
when(keycloakProvider.getIssuer()).thenReturn(""); // Empty keycloak issuer
when(keycloakProvider.getClientId()).thenReturn(clientId);
when(oauth.getIssuer()).thenReturn(customIssuerUrl); // Use custom issuer
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("keycloak");
when(authentication.getPrincipal()).thenReturn(oidcUser);
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify custom issuer is used
verify(response)
.sendRedirect(
contains(
customIssuerUrl
+ "/protocol/openid-connect/logout?id_token_hint="));
}
@Test
void testGitHubLogout_RedirectsToLocalLogout() throws IOException {
// Test that GitHub logout redirects to local logout page (no provider logout)
String redirectUrl = "http://localhost:8080/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("github");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify redirect to local logout page
verify(response).sendRedirect(redirectUrl);
}
@Test
void testGoogleLogout_RedirectsToLocalLogout() throws IOException {
// Test that Google logout redirects to local logout page (no provider logout)
String redirectUrl = "http://localhost:8080/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
OAuth2AuthenticationToken authentication = mock(OAuth2AuthenticationToken.class);
ApplicationProperties.Security.OAUTH2 oauth =
mock(ApplicationProperties.Security.OAUTH2.class);
when(response.isCommitted()).thenReturn(false);
when(request.getParameter("oAuth2AuthenticationErrorWeb")).thenReturn(null);
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getOauth2()).thenReturn(oauth);
when(authentication.getAuthorizedClientRegistrationId()).thenReturn("google");
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify redirect to local logout page
verify(response).sendRedirect(redirectUrl);
}
@Test
void testSaml2LogoutSuccess_RedirectsToIdentityProvider() throws Exception {
// Test successful SAML2 logout with redirect to IdP
// Note: This test verifies the handler processes SAML2 logout without exceptions
// In a real scenario, SamlClient would redirect to the IdP
String registrationId = "test-saml";
String providerName = "TestIdP";
String nameIdValue = "user@example.com";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Saml2Authentication authentication = mock(Saml2Authentication.class);
CustomSaml2AuthenticatedPrincipal principal = mock(CustomSaml2AuthenticatedPrincipal.class);
ApplicationProperties.Security.SAML2 saml2Config =
mock(ApplicationProperties.Security.SAML2.class);
Resource certResource = mock(Resource.class);
Resource keyResource = mock(Resource.class);
X509Certificate certificate = mock(X509Certificate.class);
RSAPrivateKey privateKey = mock(RSAPrivateKey.class);
when(response.isCommitted()).thenReturn(false);
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
when(request.getServerPort()).thenReturn(8080);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getSaml2()).thenReturn(saml2Config);
when(saml2Config.getRegistrationId()).thenReturn(registrationId);
when(saml2Config.getProvider()).thenReturn(providerName);
when(saml2Config.getSpCert()).thenReturn(certResource);
when(saml2Config.getPrivateKey()).thenReturn(keyResource);
when(saml2Config.getIdpSingleLogoutUrl()).thenReturn("https://idp.example.com/logout");
when(saml2Config.getIdpIssuer()).thenReturn("https://idp.example.com");
when(authentication.getPrincipal()).thenReturn(principal);
when(principal.name()).thenReturn(nameIdValue);
when(appConfig.getBaseUrl()).thenReturn("http://localhost");
when(appConfig.getServerPort()).thenReturn("8080");
// Use static mocking for CertificateUtils
try (MockedStatic<CertificateUtils> certUtils = mockStatic(CertificateUtils.class)) {
certUtils
.when(() -> CertificateUtils.readCertificate(certResource))
.thenReturn(certificate);
certUtils
.when(() -> CertificateUtils.readPrivateKey(keyResource))
.thenReturn(privateKey);
// This should complete without throwing an exception
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Success is verified by no exception being thrown
}
}
@Test
void testSaml2LogoutFailure_FallsBackToLocalLogout() throws Exception {
// Test SAML2 logout with exception falls back to local logout
String registrationId = "test-saml";
String providerName = "TestIdP";
String nameIdValue = "user@example.com";
String localLogoutPath = "/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Saml2Authentication authentication = mock(Saml2Authentication.class);
CustomSaml2AuthenticatedPrincipal principal = mock(CustomSaml2AuthenticatedPrincipal.class);
ApplicationProperties.Security.SAML2 saml2Config =
mock(ApplicationProperties.Security.SAML2.class);
Resource certResource = mock(Resource.class);
when(response.isCommitted()).thenReturn(false);
when(response.encodeRedirectURL(anyString())).thenAnswer(i -> i.getArguments()[0]);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getSaml2()).thenReturn(saml2Config);
when(saml2Config.getRegistrationId()).thenReturn(registrationId);
when(saml2Config.getProvider()).thenReturn(providerName);
when(saml2Config.getSpCert()).thenReturn(certResource);
when(authentication.getPrincipal()).thenReturn(principal);
when(principal.name()).thenReturn(nameIdValue);
// Simulate exception when reading certificate
try (MockedStatic<CertificateUtils> certUtils = mockStatic(CertificateUtils.class)) {
certUtils
.when(() -> CertificateUtils.readCertificate(certResource))
.thenThrow(new RuntimeException("Failed to read certificate"));
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify fallback to local logout via redirect strategy
verify(response).sendRedirect(localLogoutPath);
}
}
@Test
void testSaml2LogoutWithCertificateError_RedirectsToLocalLogout() throws Exception {
// Test SAML2 logout with certificate reading error
String registrationId = "test-saml";
String providerName = "TestIdP";
String nameIdValue = "user@example.com";
String localLogoutPath = "/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Saml2Authentication authentication = mock(Saml2Authentication.class);
CustomSaml2AuthenticatedPrincipal principal = mock(CustomSaml2AuthenticatedPrincipal.class);
ApplicationProperties.Security.SAML2 saml2Config =
mock(ApplicationProperties.Security.SAML2.class);
Resource certResource = mock(Resource.class);
when(response.isCommitted()).thenReturn(false);
when(response.encodeRedirectURL(anyString())).thenAnswer(i -> i.getArguments()[0]);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getSaml2()).thenReturn(saml2Config);
when(saml2Config.getRegistrationId()).thenReturn(registrationId);
when(saml2Config.getProvider()).thenReturn(providerName);
when(saml2Config.getSpCert()).thenReturn(certResource);
when(authentication.getPrincipal()).thenReturn(principal);
when(principal.name()).thenReturn(nameIdValue);
// Simulate certificate error
try (MockedStatic<CertificateUtils> certUtils = mockStatic(CertificateUtils.class)) {
certUtils
.when(() -> CertificateUtils.readCertificate(certResource))
.thenThrow(
new java.security.cert.CertificateException(
"Invalid certificate format"));
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify fallback to local logout via redirect strategy
verify(response).sendRedirect(localLogoutPath);
}
}
@Test
void testSaml2LogoutWithPrivateKeyError_RedirectsToLocalLogout() throws Exception {
// Test SAML2 logout with private key reading error
String registrationId = "test-saml";
String providerName = "TestIdP";
String nameIdValue = "user@example.com";
String localLogoutPath = "/login?logout=true";
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
Saml2Authentication authentication = mock(Saml2Authentication.class);
CustomSaml2AuthenticatedPrincipal principal = mock(CustomSaml2AuthenticatedPrincipal.class);
ApplicationProperties.Security.SAML2 saml2Config =
mock(ApplicationProperties.Security.SAML2.class);
Resource certResource = mock(Resource.class);
Resource keyResource = mock(Resource.class);
X509Certificate certificate = mock(X509Certificate.class);
when(response.isCommitted()).thenReturn(false);
when(response.encodeRedirectURL(anyString())).thenAnswer(i -> i.getArguments()[0]);
when(request.getContextPath()).thenReturn("");
when(securityProperties.getSaml2()).thenReturn(saml2Config);
when(saml2Config.getRegistrationId()).thenReturn(registrationId);
when(saml2Config.getProvider()).thenReturn(providerName);
when(saml2Config.getSpCert()).thenReturn(certResource);
when(saml2Config.getPrivateKey()).thenReturn(keyResource);
when(saml2Config.getIdpSingleLogoutUrl()).thenReturn("https://idp.example.com/logout");
when(saml2Config.getIdpIssuer()).thenReturn("https://idp.example.com");
when(authentication.getPrincipal()).thenReturn(principal);
when(principal.name()).thenReturn(nameIdValue);
when(appConfig.getBaseUrl()).thenReturn("http://localhost");
when(appConfig.getServerPort()).thenReturn("8080");
// Certificate reads successfully but private key fails
try (MockedStatic<CertificateUtils> certUtils = mockStatic(CertificateUtils.class)) {
certUtils
.when(() -> CertificateUtils.readCertificate(certResource))
.thenReturn(certificate);
certUtils
.when(() -> CertificateUtils.readPrivateKey(keyResource))
.thenThrow(new RuntimeException("Failed to read private key"));
customLogoutSuccessHandler.onLogoutSuccess(request, response, authentication);
// Verify fallback to local logout via redirect strategy
verify(response).sendRedirect(localLogoutPath);
}
}
}