Return JSON for API requests, redirect for browser requests
This commit is contained in:
committed by
DarioGii
parent
dba8c1f962
commit
9d895ae0ca
+138
-117
@@ -266,6 +266,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
OAUTH2 oauth = securityProperties.getOauth2();
|
||||
String path = checkForErrors(request);
|
||||
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
||||
boolean isApi = isApiRequest(request);
|
||||
|
||||
// For JWT-based auth, we don't have OAuth2AuthenticationToken
|
||||
// Attempt generic OIDC logout
|
||||
@@ -294,31 +295,37 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
log.debug("Using Keycloak fallback logout path: {}", endSessionEndpoint);
|
||||
}
|
||||
|
||||
// If we have an endpoint, construct the logout URL
|
||||
if (endSessionEndpoint != null) {
|
||||
StringBuilder logoutUrlBuilder = new StringBuilder(endSessionEndpoint);
|
||||
logoutUrlBuilder.append(endSessionEndpoint.contains("?") ? "&" : "?");
|
||||
|
||||
// Without OAuth2AuthenticationToken, we don't have id_token_hint
|
||||
// Just use client_id and post_logout_redirect_uri
|
||||
// Use client_id and post_logout_redirect_uri
|
||||
if (clientId != null && !clientId.isBlank()) {
|
||||
logoutUrlBuilder.append("client_id=").append(clientId);
|
||||
logoutUrlBuilder.append("&");
|
||||
logoutUrlBuilder.append("client_id=").append(clientId).append("&");
|
||||
}
|
||||
logoutUrlBuilder
|
||||
.append("post_logout_redirect_uri=")
|
||||
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
|
||||
String encodedRedirectUri = URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8);
|
||||
logoutUrlBuilder.append("post_logout_redirect_uri=").append(encodedRedirectUri);
|
||||
|
||||
String logoutUrl = logoutUrlBuilder.toString();
|
||||
log.info("JWT-based OAuth2 logout URL: {}", logoutUrl);
|
||||
response.sendRedirect(logoutUrl);
|
||||
|
||||
// Return JSON for API requests, redirect for browser requests
|
||||
if (isApi) {
|
||||
sendJsonLogoutResponse(response, logoutUrl);
|
||||
} else {
|
||||
response.sendRedirect(logoutUrl);
|
||||
}
|
||||
} else {
|
||||
// No OIDC logout endpoint available - fallback to local logout
|
||||
log.info(
|
||||
"No OIDC logout endpoint available for issuer: {}. Using local logout: {}",
|
||||
issuer,
|
||||
redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
if (isApi) {
|
||||
sendJsonLogoutResponse(response, redirectUrl);
|
||||
} else {
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,18 +368,21 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
log.debug("Using Keycloak fallback logout path: {}", endSessionEndpoint);
|
||||
}
|
||||
|
||||
// If we have an endpoint, construct the logout URL
|
||||
if (endSessionEndpoint != null) {
|
||||
StringBuilder logoutUrlBuilder = new StringBuilder(endSessionEndpoint);
|
||||
|
||||
// Extract id_token_hint if available (OIDC)
|
||||
// Extract id_token_hint if available
|
||||
Object principal = oAuthToken.getPrincipal();
|
||||
|
||||
if (principal instanceof OidcUser oidcUser) {
|
||||
String idToken = oidcUser.getIdToken().getTokenValue();
|
||||
logoutUrlBuilder.append(
|
||||
endSessionEndpoint.contains("?") ? "&" : "?"); // Handle existing params
|
||||
logoutUrlBuilder.append("id_token_hint=").append(idToken);
|
||||
logoutUrlBuilder
|
||||
.append(
|
||||
endSessionEndpoint.contains("?")
|
||||
? "&"
|
||||
: "?") // Handle existing params
|
||||
.append("id_token_hint=")
|
||||
.append(idToken)
|
||||
.append("&post_logout_redirect_uri=")
|
||||
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
|
||||
|
||||
@@ -382,23 +392,20 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
logoutUrlBuilder.append("&client_id=").append(clientId);
|
||||
}
|
||||
|
||||
log.info("OIDC logout with id_token_hint (session-aware): {}", endSessionEndpoint);
|
||||
log.info("Session-aware OIDC logout: {}", endSessionEndpoint);
|
||||
} else {
|
||||
// Fallback to client_id only (less ideal, may show confirmation screen)
|
||||
logoutUrlBuilder.append(endSessionEndpoint.contains("?") ? "&" : "?");
|
||||
if (clientId != null && !clientId.isBlank()) {
|
||||
logoutUrlBuilder.append("client_id=").append(clientId);
|
||||
logoutUrlBuilder.append("&");
|
||||
logoutUrlBuilder.append("client_id=").append(clientId).append("&");
|
||||
}
|
||||
logoutUrlBuilder
|
||||
.append("post_logout_redirect_uri=")
|
||||
.append(URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8));
|
||||
|
||||
log.warn("OIDC logout without id_token_hint - user may see confirmation screen");
|
||||
}
|
||||
|
||||
String logoutUrl = logoutUrlBuilder.toString();
|
||||
log.debug("OIDC logout URL: {}", logoutUrl);
|
||||
|
||||
response.sendRedirect(logoutUrl);
|
||||
} else {
|
||||
// No OIDC logout endpoint available - fallback to local logout
|
||||
@@ -410,6 +417,116 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OIDC end_session_endpoint from: 1. Configuration first 2. Fall back to discovery 3.
|
||||
* Return null if not available
|
||||
*
|
||||
* @param oauth The OAuth2 configuration
|
||||
* @param issuer The OIDC issuer URL
|
||||
* @return The end_session_endpoint URL, or null if not available
|
||||
*/
|
||||
private String getEndSessionEndpoint(
|
||||
ApplicationProperties.Security.OAUTH2 oauth, String issuer) {
|
||||
if (oauth != null && oauth.getClient() != null) {
|
||||
String configuredEndpoint = oauth.getClient().getEndSessionEndpoint();
|
||||
|
||||
if (configuredEndpoint != null && !configuredEndpoint.isBlank()) {
|
||||
log.debug("Using configured end_session_endpoint: {}", configuredEndpoint);
|
||||
return configuredEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
if (issuer != null && !issuer.isBlank()) {
|
||||
return discoverEndSessionEndpoint(issuer);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers the OIDC end_session_endpoint from the provider's .well-known/openid-configuration
|
||||
* Uses a cache to avoid repeated HTTP calls
|
||||
*
|
||||
* @param issuer The OIDC issuer URL
|
||||
* @return The end_session_endpoint URL, or null if not found/supported
|
||||
*/
|
||||
private String discoverEndSessionEndpoint(String issuer) {
|
||||
if (endSessionEndpointCache.containsKey(issuer)) {
|
||||
return endSessionEndpointCache.get(issuer);
|
||||
}
|
||||
|
||||
try {
|
||||
String discoveryUrl = issuer;
|
||||
if (!discoveryUrl.endsWith("/")) {
|
||||
discoveryUrl += "/";
|
||||
}
|
||||
discoveryUrl += ".well-known/openid-configuration";
|
||||
|
||||
log.debug("Discovery URL: {}", discoveryUrl);
|
||||
|
||||
// Make HTTP request with timeout using Spring's RestClient
|
||||
RestClient restClient =
|
||||
RestClient.builder()
|
||||
.baseUrl(discoveryUrl)
|
||||
.defaultHeaders(headers -> headers.set("Accept", "application/json"))
|
||||
.build();
|
||||
|
||||
// Fetch and parse OIDC discovery document
|
||||
Map discoveryDoc =
|
||||
restClient
|
||||
.get()
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> !status.is2xxSuccessful(),
|
||||
(request, response) ->
|
||||
log.warn(
|
||||
"Failed to discover OIDC endpoints for {}: HTTP status {}",
|
||||
issuer,
|
||||
response.getStatusCode().value()))
|
||||
.body(Map.class);
|
||||
|
||||
if (discoveryDoc != null && discoveryDoc.containsKey("end_session_endpoint")) {
|
||||
String endpoint = (String) discoveryDoc.get("end_session_endpoint");
|
||||
if (endpoint != null && !endpoint.isBlank()) {
|
||||
log.info("Discovered end_session_endpoint for {}: {}", issuer, endpoint);
|
||||
// Cache the result
|
||||
endSessionEndpointCache.put(issuer, endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Provider {} does not advertise end_session_endpoint in OIDC discovery",
|
||||
issuer);
|
||||
// Cache null result to avoid repeated failed attempts
|
||||
endSessionEndpointCache.put(issuer, null);
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Error discovering end_session_endpoint for {}: {}", issuer, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if the request expects a JSON response (API/XHR request) */
|
||||
private boolean isApiRequest(HttpServletRequest request) {
|
||||
String accept = request.getHeader("Accept");
|
||||
String xRequestedWith = request.getHeader("X-Requested-With");
|
||||
return (accept != null && accept.contains("application/json"))
|
||||
|| "XMLHttpRequest".equals(xRequestedWith);
|
||||
}
|
||||
|
||||
/** Send JSON response with logout URL for API requests */
|
||||
private void sendJsonLogoutResponse(HttpServletResponse response, String logoutUrl)
|
||||
throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
// Escape the URL for JSON
|
||||
String escapedUrl = logoutUrl.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
response.getWriter().write("{\"logoutUrl\":\"" + escapedUrl + "\"}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
||||
* the error request parameter.
|
||||
@@ -458,100 +575,4 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
.matcher(input)
|
||||
.replaceAll("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers the OIDC end_session_endpoint from the provider's .well-known/openid-configuration
|
||||
* Uses a cache to avoid repeated HTTP calls
|
||||
*
|
||||
* @param issuer The OIDC issuer URL
|
||||
* @return The end_session_endpoint URL, or null if not found/supported
|
||||
*/
|
||||
private String discoverEndSessionEndpoint(String issuer) {
|
||||
// Check cache first
|
||||
if (endSessionEndpointCache.containsKey(issuer)) {
|
||||
return endSessionEndpointCache.get(issuer);
|
||||
}
|
||||
|
||||
try {
|
||||
// Construct discovery URL
|
||||
String discoveryUrl = issuer;
|
||||
if (!discoveryUrl.endsWith("/")) {
|
||||
discoveryUrl += "/";
|
||||
}
|
||||
discoveryUrl += ".well-known/openid-configuration";
|
||||
|
||||
log.debug("Discovering OIDC endpoints from: {}", discoveryUrl);
|
||||
|
||||
// Make HTTP request with timeout using Spring's RestClient
|
||||
RestClient restClient =
|
||||
RestClient.builder()
|
||||
.baseUrl(discoveryUrl)
|
||||
.defaultHeaders(
|
||||
headers -> {
|
||||
headers.set("Accept", "application/json");
|
||||
})
|
||||
.build();
|
||||
|
||||
// Fetch and parse OIDC discovery document
|
||||
Map<String, Object> discoveryDoc =
|
||||
restClient
|
||||
.get()
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
status -> !status.is2xxSuccessful(),
|
||||
(request, response) ->
|
||||
log.warn(
|
||||
"Failed to discover OIDC endpoints for {}: HTTP {}",
|
||||
issuer,
|
||||
response.getStatusCode().value()))
|
||||
.body(Map.class);
|
||||
|
||||
if (discoveryDoc != null && discoveryDoc.containsKey("end_session_endpoint")) {
|
||||
String endpoint = (String) discoveryDoc.get("end_session_endpoint");
|
||||
if (endpoint != null && !endpoint.isBlank()) {
|
||||
log.info("Discovered end_session_endpoint for {}: {}", issuer, endpoint);
|
||||
// Cache the result
|
||||
endSessionEndpointCache.put(issuer, endpoint);
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
log.info(
|
||||
"Provider {} does not advertise end_session_endpoint in OIDC discovery",
|
||||
issuer);
|
||||
// Cache null result to avoid repeated failed attempts
|
||||
endSessionEndpointCache.put(issuer, null);
|
||||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Error discovering end_session_endpoint for {}: {}", issuer, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the OIDC end_session_endpoint from: 1. Configuration first 2. Fall back to discovery 3.
|
||||
* Return null if not available
|
||||
*
|
||||
* @param oauth The OAuth2 configuration
|
||||
* @param issuer The OIDC issuer URL
|
||||
* @return The end_session_endpoint URL, or null if not available
|
||||
*/
|
||||
private String getEndSessionEndpoint(
|
||||
ApplicationProperties.Security.OAUTH2 oauth, String issuer) {
|
||||
if (oauth != null && oauth.getClient() != null) {
|
||||
String configuredEndpoint = oauth.getClient().getEndSessionEndpoint();
|
||||
|
||||
if (configuredEndpoint != null && !configuredEndpoint.isBlank()) {
|
||||
log.debug("Using configured end_session_endpoint: {}", configuredEndpoint);
|
||||
return configuredEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
if (issuer != null && !issuer.isBlank()) {
|
||||
return discoverEndSessionEndpoint(issuer);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+212
-9
@@ -10,6 +10,8 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.time.Instant;
|
||||
@@ -378,7 +380,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
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);
|
||||
@@ -405,7 +406,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
// 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);
|
||||
@@ -426,7 +426,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
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);
|
||||
@@ -451,7 +450,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
// 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);
|
||||
@@ -480,7 +478,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
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);
|
||||
@@ -757,7 +754,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
String issuerUrl = "https://authentik.example.com/application/o/stirling-pdf/";
|
||||
String clientId = "stirling-pdf";
|
||||
String idTokenValue = "test.id.token";
|
||||
String redirectUrl = "http://localhost:8080/login?logout=true";
|
||||
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
@@ -784,7 +780,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
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);
|
||||
@@ -812,7 +807,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
String discoveredEndpoint = "https://authentik.example.com/application/o/end-session/";
|
||||
String clientId = "stirling-pdf";
|
||||
String idTokenValue = "test.id.token";
|
||||
String redirectUrl = "http://localhost:8080/login?logout=true";
|
||||
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
@@ -839,7 +833,6 @@ class CustomLogoutSuccessHandlerTest {
|
||||
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);
|
||||
@@ -1007,4 +1000,214 @@ class CustomLogoutSuccessHandlerTest {
|
||||
verify(response).sendRedirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJwtLogout_ApiRequest_ReturnsJsonWithLogoutUrl() throws IOException {
|
||||
// Test that API requests (Accept: application/json) get JSON response with logout URL
|
||||
String issuerUrl = "https://keycloak.example.com/realms/test";
|
||||
String clientId = "stirling-pdf";
|
||||
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||
jwtAuth =
|
||||
mock(
|
||||
org.springframework.security.oauth2.server.resource.authentication
|
||||
.JwtAuthenticationToken.class);
|
||||
org.springframework.security.oauth2.jwt.Jwt jwt =
|
||||
mock(org.springframework.security.oauth2.jwt.Jwt.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);
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
PrintWriter printWriter = new PrintWriter(stringWriter);
|
||||
|
||||
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(request.getHeader("Accept")).thenReturn("application/json"); // API request
|
||||
when(request.getHeader("X-Requested-With")).thenReturn(null);
|
||||
when(response.getWriter()).thenReturn(printWriter);
|
||||
|
||||
when(jwtAuth.getToken()).thenReturn(jwt);
|
||||
when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2"));
|
||||
|
||||
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||
when(oauth.getClient()).thenReturn(client);
|
||||
when(client.getEndSessionEndpoint()).thenReturn(null);
|
||||
when(client.getKeycloak()).thenReturn(keycloakProvider);
|
||||
when(keycloakProvider.getIssuer()).thenReturn(issuerUrl);
|
||||
when(keycloakProvider.getClientId()).thenReturn(clientId);
|
||||
|
||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, jwtAuth);
|
||||
|
||||
// Verify JSON response
|
||||
verify(response).setStatus(HttpServletResponse.SC_OK);
|
||||
verify(response).setContentType("application/json");
|
||||
verify(response).setCharacterEncoding("UTF-8");
|
||||
verify(response).getWriter();
|
||||
|
||||
String jsonResponse = stringWriter.toString();
|
||||
assert jsonResponse.contains("\"logoutUrl\":");
|
||||
assert jsonResponse.contains(issuerUrl);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJwtLogout_XhrRequest_ReturnsJsonWithLogoutUrl() throws IOException {
|
||||
// Test that XHR requests (X-Requested-With: XMLHttpRequest) get JSON response
|
||||
String issuerUrl = "https://keycloak.example.com/realms/test";
|
||||
String clientId = "stirling-pdf";
|
||||
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||
jwtAuth =
|
||||
mock(
|
||||
org.springframework.security.oauth2.server.resource.authentication
|
||||
.JwtAuthenticationToken.class);
|
||||
org.springframework.security.oauth2.jwt.Jwt jwt =
|
||||
mock(org.springframework.security.oauth2.jwt.Jwt.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);
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
PrintWriter printWriter = new PrintWriter(stringWriter);
|
||||
|
||||
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(request.getHeader("Accept")).thenReturn("text/html"); // Not JSON Accept header
|
||||
when(request.getHeader("X-Requested-With")).thenReturn("XMLHttpRequest"); // XHR request
|
||||
when(response.getWriter()).thenReturn(printWriter);
|
||||
|
||||
when(jwtAuth.getToken()).thenReturn(jwt);
|
||||
when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2"));
|
||||
|
||||
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||
when(oauth.getClient()).thenReturn(client);
|
||||
when(client.getEndSessionEndpoint()).thenReturn(null);
|
||||
when(client.getKeycloak()).thenReturn(keycloakProvider);
|
||||
when(keycloakProvider.getIssuer()).thenReturn(issuerUrl);
|
||||
when(keycloakProvider.getClientId()).thenReturn(clientId);
|
||||
|
||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, jwtAuth);
|
||||
|
||||
// Verify JSON response
|
||||
verify(response).setStatus(HttpServletResponse.SC_OK);
|
||||
verify(response).setContentType("application/json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJwtLogout_BrowserRequest_RedirectsToLogoutUrl() throws IOException {
|
||||
// Test that browser requests (no Accept: application/json) get redirected
|
||||
String issuerUrl = "https://keycloak.example.com/realms/test";
|
||||
String clientId = "stirling-pdf";
|
||||
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||
jwtAuth =
|
||||
mock(
|
||||
org.springframework.security.oauth2.server.resource.authentication
|
||||
.JwtAuthenticationToken.class);
|
||||
org.springframework.security.oauth2.jwt.Jwt jwt =
|
||||
mock(org.springframework.security.oauth2.jwt.Jwt.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);
|
||||
|
||||
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(request.getHeader("Accept")).thenReturn("text/html"); // Browser request
|
||||
when(request.getHeader("X-Requested-With")).thenReturn(null);
|
||||
|
||||
when(jwtAuth.getToken()).thenReturn(jwt);
|
||||
when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2"));
|
||||
|
||||
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||
when(oauth.getClient()).thenReturn(client);
|
||||
when(client.getEndSessionEndpoint()).thenReturn(null);
|
||||
when(client.getKeycloak()).thenReturn(keycloakProvider);
|
||||
when(keycloakProvider.getIssuer()).thenReturn(issuerUrl);
|
||||
when(keycloakProvider.getClientId()).thenReturn(clientId);
|
||||
|
||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, jwtAuth);
|
||||
|
||||
// Verify redirect (not JSON)
|
||||
verify(response).sendRedirect(contains(issuerUrl + "/protocol/openid-connect/logout"));
|
||||
verify(response).sendRedirect(contains("client_id=" + clientId));
|
||||
verify(response).sendRedirect(contains("post_logout_redirect_uri="));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testJwtLogout_ApiRequest_NoOidcEndpoint_ReturnsLocalLogoutUrl() throws IOException {
|
||||
// Test that API requests with no OIDC endpoint return local logout URL as JSON
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
|
||||
jwtAuth =
|
||||
mock(
|
||||
org.springframework.security.oauth2.server.resource.authentication
|
||||
.JwtAuthenticationToken.class);
|
||||
org.springframework.security.oauth2.jwt.Jwt jwt =
|
||||
mock(org.springframework.security.oauth2.jwt.Jwt.class);
|
||||
ApplicationProperties.Security.OAUTH2 oauth =
|
||||
mock(ApplicationProperties.Security.OAUTH2.class);
|
||||
ApplicationProperties.Security.OAUTH2.Client client =
|
||||
mock(ApplicationProperties.Security.OAUTH2.Client.class);
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
PrintWriter printWriter = new PrintWriter(stringWriter);
|
||||
|
||||
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(request.getHeader("Accept")).thenReturn("application/json");
|
||||
when(request.getHeader("X-Requested-With")).thenReturn(null);
|
||||
when(response.getWriter()).thenReturn(printWriter);
|
||||
|
||||
when(jwtAuth.getToken()).thenReturn(jwt);
|
||||
when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2"));
|
||||
|
||||
when(securityProperties.getOauth2()).thenReturn(oauth);
|
||||
when(oauth.getClient()).thenReturn(client);
|
||||
when(client.getEndSessionEndpoint()).thenReturn(null);
|
||||
when(client.getKeycloak()).thenReturn(null); // No Keycloak configured
|
||||
when(oauth.getIssuer()).thenReturn(""); // No issuer
|
||||
|
||||
customLogoutSuccessHandler.onLogoutSuccess(request, response, jwtAuth);
|
||||
|
||||
// Verify JSON response with local logout URL
|
||||
verify(response).setStatus(HttpServletResponse.SC_OK);
|
||||
verify(response).setContentType("application/json");
|
||||
|
||||
String jsonResponse = stringWriter.toString();
|
||||
assert jsonResponse.contains("\"logoutUrl\":");
|
||||
assert jsonResponse.contains("/login?logout=true");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user