This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 4_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 92de7f1b0aebe07fe192561cf5045f9f0dafec4e
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Wed Sep 9 14:22:52 2026 +0200

    SRA: require JWKS URI for OAuth 2.0
---
 .../markup/html/bootstrap/dialog/BaseModal.java    |  2 +-
 .../apache/syncope/fit/sra/AbstractOIDCITCase.java | 88 +---------------------
 .../apache/syncope/fit/sra/OAUTH2SRAITCase.java    |  5 --
 .../org/apache/syncope/fit/sra/OIDCSRAITCase.java  | 86 +++++++++++++++++++--
 .../src/test/resources/sra-oauth2.properties       |  2 +-
 .../org/apache/syncope/sra/SecurityConfig.java     | 17 ++---
 6 files changed, 89 insertions(+), 111 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
index 0fe0aeeb29..d5abfa7ece 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wicket/markup/html/bootstrap/dialog/BaseModal.java
@@ -108,7 +108,7 @@ public class BaseModal<T extends Serializable> extends 
Modal<T> {
 
         defaultModalCloseButton = new DefaultModalCloseButton();
         addButton(defaultModalCloseButton);
-        setUseKeyboard(true);
+        setCloseOnEscapeKey(true);
         setFadeIn(true);
         setBackdrop(Modal.Backdrop.STATIC);
     }
diff --git 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractOIDCITCase.java
 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractOIDCITCase.java
index e8ee0f62aa..27516f9a7e 100644
--- 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractOIDCITCase.java
+++ 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractOIDCITCase.java
@@ -19,28 +19,19 @@
 package org.apache.syncope.fit.sra;
 
 import static org.awaitility.Awaitility.await;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.oneOf;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
-import jakarta.ws.rs.core.Form;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import java.io.IOException;
-import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import org.apache.cxf.jaxrs.client.WebClient;
@@ -69,7 +60,6 @@ import org.apache.syncope.common.lib.types.OIDCSubjectType;
 import org.apache.syncope.common.lib.types.PolicyType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.service.wa.WAConfigService;
-import org.apereo.cas.support.oauth.OAuth20Constants;
 import org.jsoup.Jsoup;
 import org.junit.jupiter.api.Test;
 
@@ -99,7 +89,7 @@ abstract class AbstractOIDCITCase extends AbstractSRAITCase {
         return POLICY_SERVICE.list(PolicyType.ATTR_RELEASE).stream().
                 map(AttrReleasePolicyTO.class::cast).
                 filter(policy -> description.equals(policy.getName())
-                && policy.getConf() instanceof DefaultAttrReleasePolicyConf).
+                        && policy.getConf() instanceof 
DefaultAttrReleasePolicyConf).
                 findFirst().
                 orElseGet(() -> {
                     DefaultAttrReleasePolicyConf policyConf = new 
DefaultAttrReleasePolicyConf();
@@ -286,80 +276,4 @@ abstract class AbstractOIDCITCase extends 
AbstractSRAITCase {
         response = httpclient.execute(get, context);
         checkLogout(response);
     }
-
-    @SuppressWarnings("unchecked")
-    private void checkJWT(final String token, final boolean idToken) throws 
ParseException {
-        assertNotNull(token);
-        SignedJWT jwt = SignedJWT.parse(token);
-        assertNotNull(jwt);
-        JWTClaimsSet idTokenClaimsSet = jwt.getJWTClaimsSet();
-        assertEquals("verdi", idTokenClaimsSet.getSubject());
-        if (idToken) {
-            assertEquals("verdi", 
idTokenClaimsSet.getStringClaim("preferred_username"));
-        }
-        assertEquals("[email protected]", 
idTokenClaimsSet.getStringClaim("email"));
-        assertEquals("Verdi", idTokenClaimsSet.getStringClaim("family_name"));
-        assertEquals("Giuseppe", 
idTokenClaimsSet.getStringClaim("given_name"));
-        assertEquals("Giuseppe Verdi", 
idTokenClaimsSet.getStringClaim("name"));
-        List<Object> groups = idTokenClaimsSet.getListClaim("groups");
-        assertEquals(3, groups.size());
-        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "root")));
-        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "child")));
-        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "citizen")));
-    }
-
-    protected abstract boolean checkIdToken();
-
-    @Test
-    void rest() throws IOException, ParseException {
-        // 0. access public route
-        WebClient client = WebClient.create(SRA_ADDRESS + "/public/post").
-                
accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
-        Response response = client.post(null);
-        assertEquals(HttpStatus.SC_OK, response.getStatus());
-
-        // 1. obtain id and access tokens
-        Form form = new Form().
-                param(OAuth20Constants.GRANT_TYPE, 
OIDCGrantType.password.getExternalForm()).
-                param(OAuth20Constants.CLIENT_ID, SRA_CLIENT_ID).
-                param(OAuth20Constants.CLIENT_SECRET, SRA_CLIENT_SECRET).
-                param("username", "verdi").
-                param("password", "password").
-                param(OAuth20Constants.SCOPE, "openid profile email " + 
GROUPS_SCOPE);
-        response = WebClient.create(TOKEN_URI).post(form);
-        assertEquals(HttpStatus.SC_OK, response.getStatus());
-        
assertTrue(response.getHeaderString(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON));
-
-        JsonNode json = MAPPER.readTree(response.readEntity(String.class));
-
-        if (checkIdToken()) {
-            // 1a. take and verify id_token
-            String idToken = json.get("id_token").asText();
-            assertNotNull(idToken);
-            checkJWT(idToken, true);
-        }
-
-        // 1b. take and verify access_token
-        String accessToken = json.get("access_token").asText();
-        checkJWT(accessToken, false);
-
-        // 2. access protected route
-        client = WebClient.create(SRA_ADDRESS + "/protected/post").
-                authorization("Bearer " + accessToken).
-                
accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
-        response = client.post(null);
-
-        assertEquals(HttpStatus.SC_OK, response.getStatus());
-
-        json = MAPPER.readTree(response.readEntity(String.class));
-
-        ObjectNode headers = (ObjectNode) json.get("headers");
-        assertEquals(MediaType.APPLICATION_JSON, 
headers.get(HttpHeaders.ACCEPT).asText());
-        assertEquals(MediaType.APPLICATION_JSON, 
headers.get(HttpHeaders.CONTENT_TYPE).asText());
-        assertThat(headers.get("X-Forwarded-Host").asText(), 
is(oneOf("localhost:8080", "127.0.0.1:8080")));
-
-        String withHost = 
client.getBaseURI().toASCIIString().replace("/protected", "");
-        String withIP = withHost.replace("localhost", "127.0.0.1");
-        assertThat(json.get("url").asText(), is(oneOf(withHost, withIP)));
-    }
 }
diff --git 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
index aa2689b0fe..dac2827b0b 100644
--- 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
+++ 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OAUTH2SRAITCase.java
@@ -68,9 +68,4 @@ class OAUTH2SRAITCase extends AbstractOIDCITCase {
     protected void checkLogout(final CloseableHttpResponse response) {
         assertEquals(HttpStatus.SC_NOT_FOUND, 
response.getStatusLine().getStatusCode());
     }
-
-    @Override
-    protected boolean checkIdToken() {
-        return false;
-    }
 }
diff --git 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
index 2e1c026ddb..efdae0f753 100644
--- 
a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
+++ 
b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java
@@ -18,6 +18,9 @@
  */
 package org.apache.syncope.fit.sra;
 
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.oneOf;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
@@ -27,13 +30,19 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
 import java.text.ParseException;
+import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.TimeoutException;
@@ -133,11 +142,6 @@ class OIDCSRAITCase extends AbstractOIDCITCase {
                 OIDCSRAITCase.class.getName(), SRA_REGISTRATION_ID, 
CLIENT_APP_ID, SRA_CLIENT_ID, SRA_CLIENT_SECRET);
     }
 
-    @Override
-    protected boolean checkIdToken() {
-        return true;
-    }
-
     @Test
     public void clientCredentials() throws JsonProcessingException, 
ParseException {
         WebClient webclient = WebClient.create(WA_ADDRESS + 
"/oidc/oidcAccessToken");
@@ -161,4 +165,76 @@ class OIDCSRAITCase extends AbstractOIDCITCase {
         assertNotNull(idToken.getJWTClaimsSet().getClaim(CUSTOM_CLAIM1));
         assertNull(idToken.getJWTClaimsSet().getClaim(CUSTOM_CLAIM2));
     }
+
+    @SuppressWarnings("unchecked")
+    private void checkJWT(final String token, final boolean idToken) throws 
ParseException {
+        assertNotNull(token);
+        SignedJWT jwt = SignedJWT.parse(token);
+        assertNotNull(jwt);
+        JWTClaimsSet idTokenClaimsSet = jwt.getJWTClaimsSet();
+        assertEquals("verdi", idTokenClaimsSet.getSubject());
+        if (idToken) {
+            assertEquals("verdi", 
idTokenClaimsSet.getStringClaim("preferred_username"));
+        }
+        assertEquals("[email protected]", 
idTokenClaimsSet.getStringClaim("email"));
+        assertEquals("Verdi", idTokenClaimsSet.getStringClaim("family_name"));
+        assertEquals("Giuseppe", 
idTokenClaimsSet.getStringClaim("given_name"));
+        assertEquals("Giuseppe Verdi", 
idTokenClaimsSet.getStringClaim("name"));
+        List<Object> groups = idTokenClaimsSet.getListClaim("groups");
+        assertEquals(3, groups.size());
+        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "root")));
+        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "child")));
+        groups.stream().anyMatch(g -> ((Map<String, String>) 
g).equals(Map.of("groupName", "citizen")));
+    }
+
+    @Test
+    void rest() throws IOException, ParseException {
+        // 0. access public route
+        WebClient client = WebClient.create(SRA_ADDRESS + "/public/post").
+                
accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
+        Response response = client.post(null);
+        assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+        // 1. obtain id and access tokens
+        Form form = new Form().
+                param(OAuth20Constants.GRANT_TYPE, 
OIDCGrantType.password.getExternalForm()).
+                param(OAuth20Constants.CLIENT_ID, SRA_CLIENT_ID).
+                param(OAuth20Constants.CLIENT_SECRET, SRA_CLIENT_SECRET).
+                param("username", "verdi").
+                param("password", "password").
+                param(OAuth20Constants.SCOPE, "openid profile email " + 
GROUPS_SCOPE);
+        response = WebClient.create(TOKEN_URI).post(form);
+        assertEquals(HttpStatus.SC_OK, response.getStatus());
+        
assertTrue(response.getHeaderString(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON));
+
+        JsonNode json = MAPPER.readTree(response.readEntity(String.class));
+
+        // 1a. take and verify id_token
+        String idToken = json.get("id_token").asText();
+        assertNotNull(idToken);
+        checkJWT(idToken, true);
+
+        // 1b. take and verify access_token
+        String accessToken = json.get("access_token").asText();
+        checkJWT(accessToken, false);
+
+        // 2. access protected route
+        client = WebClient.create(SRA_ADDRESS + "/protected/post").
+                authorization("Bearer " + accessToken).
+                
accept(MediaType.APPLICATION_JSON).type(MediaType.APPLICATION_JSON);
+        response = client.post(null);
+
+        assertEquals(HttpStatus.SC_OK, response.getStatus());
+
+        json = MAPPER.readTree(response.readEntity(String.class));
+
+        ObjectNode headers = (ObjectNode) json.get("headers");
+        assertEquals(MediaType.APPLICATION_JSON, 
headers.get(HttpHeaders.ACCEPT).asText());
+        assertEquals(MediaType.APPLICATION_JSON, 
headers.get(HttpHeaders.CONTENT_TYPE).asText());
+        assertThat(headers.get("X-Forwarded-Host").asText(), 
is(oneOf("localhost:8080", "127.0.0.1:8080")));
+
+        String withHost = 
client.getBaseURI().toASCIIString().replace("/protected", "");
+        String withIP = withHost.replace("localhost", "127.0.0.1");
+        assertThat(json.get("url").asText(), is(oneOf(withHost, withIP)));
+    }
 }
diff --git a/fit/wa-reference/src/test/resources/sra-oauth2.properties 
b/fit/wa-reference/src/test/resources/sra-oauth2.properties
index bc527e2210..dc72ba2a5e 100644
--- a/fit/wa-reference/src/test/resources/sra-oauth2.properties
+++ b/fit/wa-reference/src/test/resources/sra-oauth2.properties
@@ -28,7 +28,7 @@ 
sra.oauth2.authorizationUri=https://localhost:9443/syncope-wa/oauth2.0/authorize
 sra.oauth2.userInfoUri=https://localhost:9443/syncope-wa/oauth2.0/profile
 sra.oauth2.userNameAttributeName=id
 sra.oauth2.scopes=
-sra.oauth2.jwkSetUri=
+sra.oauth2.jwkSetUri=https://localhost:9443/syncope-wa/oidc/jwks
 sra.oauth2.issuer=https://localhost:9443/syncope-wa
 sra.oauth2.client-id=oauth2TestClientId
 sra.oauth2.client-secret=oauth2TestClientSecret
diff --git a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java 
b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
index d0bfcc5c66..9a2644363f 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java
@@ -23,7 +23,6 @@ import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.Security;
 import java.security.cert.X509Certificate;
-import java.text.ParseException;
 import java.util.Map;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
@@ -78,7 +77,6 @@ import 
org.springframework.security.web.server.SecurityWebFilterChain;
 import 
org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
 import 
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
 import 
org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
-import reactor.core.publisher.Mono;
 
 @EnableWebFluxSecurity
 @Configuration(proxyBeanMethods = false)
@@ -228,18 +226,13 @@ public class SecurityConfig {
             final Converter<Map<String, Object>, Map<String, Object>> 
jwtClaimSetConverter) {
 
         String jwkSetUri = 
oauth2ClientRegistration.getProviderDetails().getJwkSetUri();
-        NimbusReactiveJwtDecoder jwtDecoder;
         if (StringUtils.isBlank(jwkSetUri)) {
-            jwtDecoder = new NimbusReactiveJwtDecoder(jwt -> {
-                try {
-                    return Mono.just(jwt.getJWTClaimsSet());
-                } catch (ParseException e) {
-                    return Mono.error(e);
-                }
-            });
-        } else {
-            jwtDecoder = 
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
+            throw new IllegalStateException(
+                    "sra.oauth2.jwkSetUri must be configured for OAuth2 JWT 
verification."
+                    + "SRA cannot securely operate without JWT signature 
verification.");
         }
+
+        NimbusReactiveJwtDecoder jwtDecoder = 
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
         jwtDecoder.setJwtValidator(oauth2JWTValidator);
         jwtDecoder.setClaimSetConverter(jwtClaimSetConverter);
         return jwtDecoder;

Reply via email to