This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch 4_2_X in repository https://gitbox.apache.org/repos/asf/syncope.git
commit bd1769912681b4bcba8cac1316c223ca2b5ceb6c Author: Matteo Tatoni <[email protected]> AuthorDate: Mon Nov 2 10:54:00 2026 +0100 [SYNCOPE-2004] - Fix OIDCC4UI back-channel logout token validator initialization (#1594) --- .../syncope/core/logic/oidc/OIDCClientCache.java | 1 + .../org/apache/syncope/fit/ui/OIDCC4UIITCase.java | 88 +++++++++++++++++++--- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCClientCache.java b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCClientCache.java index 466df3092d..c1e3796841 100644 --- a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCClientCache.java +++ b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCClientCache.java @@ -146,6 +146,7 @@ public class OIDCClientCache { return new TokenValidator(configuration, metadata); } }); + ((StaticOidcOpMetadataResolver) cfg.getOpMetadataResolver()).init(); OidcClient client = new OidcClient(cfg); client.setName(op.getName()); diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/OIDCC4UIITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/OIDCC4UIITCase.java index 8a65494bb3..91dbc8e2e7 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/OIDCC4UIITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/OIDCC4UIITCase.java @@ -28,6 +28,7 @@ import static org.junit.jupiter.api.Assertions.fail; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -37,6 +38,7 @@ import java.util.stream.Stream; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.Consts; import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; @@ -54,6 +56,7 @@ import org.apache.http.util.EntityUtils; import org.apache.syncope.client.ui.commons.panels.OIDCC4UIConstants; import org.apache.syncope.common.lib.OIDCStandardScope; import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.to.AccessTokenTO; import org.apache.syncope.common.lib.to.Item; import org.apache.syncope.common.lib.to.OIDCC4UIProviderTO; import org.apache.syncope.common.lib.to.OIDCOpEntityTO; @@ -64,9 +67,12 @@ import org.apache.syncope.common.lib.types.LogoutType; import org.apache.syncope.common.lib.types.OIDCResponseType; import org.apache.syncope.common.lib.types.OIDCSubjectType; import org.apache.syncope.common.rest.api.RESTHeaders; +import org.apache.syncope.common.rest.api.beans.AccessTokenQuery; +import org.apache.syncope.common.rest.api.service.AccessTokenService; import org.apache.syncope.common.rest.api.service.wa.WAConfigService; import org.jsoup.Jsoup; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; public class OIDCC4UIITCase extends AbstractUIITCase { @@ -243,7 +249,8 @@ public class OIDCC4UIITCase extends AbstractUIITCase { final String baseURL, final String loginPageBody, final String username, - final String password) throws IOException { + final String password, + final boolean backChannelLogout) throws IOException { // 1. redirected to WA login screen Mutable<String> location = new MutableObject<>(); @@ -315,12 +322,75 @@ public class OIDCC4UIITCase extends AbstractUIITCase { assertTrue(EntityUtils.toString(response.getEntity()).contains(username)); } - // 4. logout - get = new HttpGet(CONSOLE_ADDRESS.equals(baseURL) - ? baseURL + "wicket/bookmarkable/org.apache.syncope.client.console.pages.Logout" - : baseURL + "wicket/bookmarkable/org.apache.syncope.client.enduser.pages.Logout"); - try (CloseableHttpResponse response = httpclient.execute(get, context)) { - assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + if (backChannelLogout) { + String accessToken = context.getRedirectLocations().stream(). + flatMap(uri -> URLEncodedUtils.parse(uri, StandardCharsets.UTF_8).stream()). + filter(param -> OIDCC4UIConstants.OIDCC4UI_JWT.equals(param.getName())). + map(NameValuePair::getValue). + findFirst().orElseThrow(() -> new AssertionError("No Syncope JWT in console login redirect")); + + // Confirm that the console's token works before logging out directly from WA. + try (Response self = WebClient.create(CORE_ADDRESS + "/users/self"). + header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken).get()) { + assertEquals(HttpStatus.SC_OK, self.getStatus()); + } + try (CloseableHttpResponse logout = httpclient.execute(new HttpGet(WA_ADDRESS + "/logout"), context)) { + assertEquals(HttpStatus.SC_OK, logout.getStatusLine().getStatusCode()); + EntityUtils.consume(logout.getEntity()); + } + + // WA sends back-channel logout asynchronously; the same JWT must then be rejected by Core. + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + try (Response self = WebClient.create(CORE_ADDRESS + "/users/self"). + header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken).get()) { + assertEquals(HttpStatus.SC_UNAUTHORIZED, self.getStatus()); + } + }); + } else { + // 4. logout + get = new HttpGet(CONSOLE_ADDRESS.equals(baseURL) + ? baseURL + "wicket/bookmarkable/org.apache.syncope.client.console.pages.Logout" + : baseURL + "wicket/bookmarkable/org.apache.syncope.client.enduser.pages.Logout"); + try (CloseableHttpResponse response = httpclient.execute(get, context)) { + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + } + } + + @Test + public void backChannelLogout() throws IOException { + // Start with a fresh Syncope token so its key matches the new OIDC session ID. + AccessTokenService accessTokenService = ADMIN_CLIENT.getService(AccessTokenService.class); + accessTokenService.list(new AccessTokenQuery.Builder().size(1000).build()).getResult().stream(). + filter(token -> "bellini".equals(token.getOwner())). + map(AccessTokenTO::getKey). + forEach(accessTokenService::delete); + + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpClientContext context = HttpClientContext.create(); + context.setCookieStore(new BasicCookieStore()); + + // 1. fetch login page + HttpGet get = new HttpGet(CONSOLE_ADDRESS); + try (CloseableHttpResponse response = httpclient.execute(get, context)) { + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } + + // 2. click on the OpenID Connect Provider + get = new HttpGet(CONSOLE_ADDRESS + OIDCC4UIConstants.URL_CONTEXT + + "/login?op=" + getAppName(CONSOLE_ADDRESS)); + get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + String loginPageBody; + try (CloseableHttpResponse response = httpclient.execute(get, context)) { + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + loginPageBody = EntityUtils.toString(response.getEntity()); + } + assertNotNull(loginPageBody); + + // 3. login and logout + loginlogout(httpclient, context, CONSOLE_ADDRESS, loginPageBody, "bellini", "password", true); } } @@ -349,7 +419,7 @@ public class OIDCC4UIITCase extends AbstractUIITCase { assertNotNull(loginPageBody); // 3. login and logout - loginlogout(httpclient, context, baseURL, loginPageBody, username, password); + loginlogout(httpclient, context, baseURL, loginPageBody, username, password, false); } } @@ -447,7 +517,7 @@ public class OIDCC4UIITCase extends AbstractUIITCase { assertTrue(loginPageBody.contains("password")); // 5. login and logout with new password - loginlogout(httpclient, context, baseURL, loginPageBody, username, newpassword); + loginlogout(httpclient, context, baseURL, loginPageBody, username, newpassword, false); } }
