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

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


The following commit(s) were added to refs/heads/4_0_X by this push:
     new 3d6df45546 [SYNCOPE-1900] Supporting OIDC Back-Channel Logout for 
Console and Enduser (#1148)
3d6df45546 is described below

commit 3d6df45546068c36d09369b7e95528a41a3fe542
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Thu Jul 31 15:30:23 2025 +0200

    [SYNCOPE-1900] Supporting OIDC Back-Channel Logout for Console and Enduser 
(#1148)
---
 .../syncope/core/logic/AccessTokenLogic.java       |  6 +-
 .../api/data/AccessTokenDataBinder.java            |  3 +-
 .../java/data/AccessTokenDataBinderImpl.java       |  4 +-
 .../spring/security/SyncopeJWTSSOProvider.java     | 29 ++++----
 .../commons/resources/oidcc4ui/LogoutResource.java | 45 ++++++++++++-
 .../syncope/common/lib/oidc/OIDCConstants.java     |  2 +
 .../apache/syncope/core/logic/OIDCC4UILogic.java   | 77 +++++++++++++++++++---
 .../syncope/core/logic/OIDCC4UILogicContext.java   |  3 +
 .../syncope/core/logic/oidc/OIDCClientCache.java   | 59 +++++++++++++----
 .../common/rest/api/service/OIDCC4UIService.java   | 13 ++++
 .../core/rest/cxf/service/OIDCC4UIServiceImpl.java | 11 ++--
 .../apache/syncope/core/logic/SAML2SP4UILogic.java |  4 +-
 .../org/apache/syncope/fit/ui/OIDCC4UIITCase.java  | 10 ++-
 pom.xml                                            |  4 +-
 wa/starter/src/main/resources/wa.properties        |  2 +
 15 files changed, 218 insertions(+), 54 deletions(-)

diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java
 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java
index 53c117f3d9..074bb7ecfa 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AccessTokenLogic.java
@@ -20,7 +20,8 @@ package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
 import java.time.OffsetDateTime;
-import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.AccessTokenTO;
@@ -83,8 +84,9 @@ public class AccessTokenLogic extends 
AbstractTransactionalLogic<AccessTokenTO>
         }
 
         return binder.create(
+                Optional.empty(),
                 AuthContextUtils.getUsername(),
-                Collections.emptyMap(),
+                Map.of(),
                 getAuthorities(),
                 false);
     }
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AccessTokenDataBinder.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AccessTokenDataBinder.java
index a487085773..a24bc035b7 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AccessTokenDataBinder.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/AccessTokenDataBinder.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.provisioning.api.data;
 
 import java.time.OffsetDateTime;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.to.AccessTokenTO;
 import org.apache.syncope.core.persistence.api.entity.AccessToken;
@@ -30,7 +31,7 @@ public interface AccessTokenDataBinder {
             String tokenId, String subject, long duration, Map<String, Object> 
claims);
 
     Pair<String, OffsetDateTime> create(
-            String subject, Map<String, Object> claims, byte[] authorities, 
boolean replace);
+            Optional<String> key, String subject, Map<String, Object> claims, 
byte[] authorities, boolean replace);
 
     Pair<String, OffsetDateTime> update(AccessToken accessToken, byte[] 
authorities);
 
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AccessTokenDataBinderImpl.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AccessTokenDataBinderImpl.java
index acedf13f36..aa8a42f68e 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AccessTokenDataBinderImpl.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AccessTokenDataBinderImpl.java
@@ -26,6 +26,7 @@ import java.text.ParseException;
 import java.time.OffsetDateTime;
 import java.util.Date;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.common.lib.SyncopeClientException;
@@ -130,6 +131,7 @@ public class AccessTokenDataBinderImpl implements 
AccessTokenDataBinder {
 
     @Override
     public Pair<String, OffsetDateTime> create(
+            final Optional<String> key,
             final String subject,
             final Map<String, Object> claims,
             final byte[] authorities,
@@ -149,7 +151,7 @@ public class AccessTokenDataBinderImpl implements 
AccessTokenDataBinder {
                 orElseGet(() -> {
                     // no AccessToken found: create new
                     AccessToken at = 
entityFactory.newEntity(AccessToken.class);
-                    
at.setKey(SecureRandomUtils.generateRandomUUID().toString());
+                    at.setKey(key.orElseGet(() -> 
SecureRandomUtils.generateRandomUUID().toString()));
 
                     return replace(subject, claims, authorities, at);
                 });
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
index 680038ce8b..8acb896423 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SyncopeJWTSSOProvider.java
@@ -37,6 +37,7 @@ import 
org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSVerifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import 
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
@@ -97,23 +98,23 @@ public class SyncopeJWTSSOProvider implements 
JWTSSOProvider {
     @Transactional(readOnly = true)
     @Override
     public Pair<User, Set<SyncopeGrantedAuthority>> resolve(final JWTClaimsSet 
jwtClaims) {
-        User user = 
userDAO.findByUsername(jwtClaims.getSubject()).orElse(null);
+        AccessToken accessToken = 
accessTokenDAO.findById(jwtClaims.getJWTID()).
+                orElseThrow(() -> new 
AuthenticationCredentialsNotFoundException(
+                "Could not find an Access Token for JWT " + 
jwtClaims.getJWTID()));
+
         Set<SyncopeGrantedAuthority> authorities = Set.of();
-        if (user != null) {
-            AccessToken accessToken = 
accessTokenDAO.findById(jwtClaims.getJWTID()).orElse(null);
-            if (accessToken != null && accessToken.getAuthorities() != null) {
-                try {
-                    authorities = POJOHelper.deserialize(
-                            encryptorManager.getInstance().decode(
-                                    new String(accessToken.getAuthorities()), 
CipherAlgorithm.AES),
-                            new TypeReference<>() {
-                    });
-                } catch (Throwable t) {
-                    LOG.error("Could not read stored authorities", t);
-                }
+        if (accessToken.getAuthorities() != null) {
+            try {
+                authorities = POJOHelper.deserialize(
+                        encryptorManager.getInstance().decode(
+                                new String(accessToken.getAuthorities()), 
CipherAlgorithm.AES),
+                        new TypeReference<>() {
+                });
+            } catch (Throwable t) {
+                LOG.error("Could not read stored authorities", t);
             }
         }
 
-        return Pair.of(user, authorities);
+        return 
Pair.of(userDAO.findByUsername(jwtClaims.getSubject()).orElse(null), 
authorities);
     }
 }
diff --git 
a/ext/oidcc4ui/client-common-ui/src/main/java/org/apache/syncope/client/ui/commons/resources/oidcc4ui/LogoutResource.java
 
b/ext/oidcc4ui/client-common-ui/src/main/java/org/apache/syncope/client/ui/commons/resources/oidcc4ui/LogoutResource.java
index 2a602b7169..784ad769f2 100644
--- 
a/ext/oidcc4ui/client-common-ui/src/main/java/org/apache/syncope/client/ui/commons/resources/oidcc4ui/LogoutResource.java
+++ 
b/ext/oidcc4ui/client-common-ui/src/main/java/org/apache/syncope/client/ui/commons/resources/oidcc4ui/LogoutResource.java
@@ -18,19 +18,62 @@
  */
 package org.apache.syncope.client.ui.commons.resources.oidcc4ui;
 
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.Optional;
+import org.apache.syncope.client.ui.commons.BaseSession;
+import org.apache.syncope.common.lib.oidc.OIDCConstants;
+import org.apache.syncope.common.rest.api.service.OIDCC4UIService;
 import org.apache.wicket.RestartResponseException;
+import org.apache.wicket.Session;
 import org.apache.wicket.markup.html.WebPage;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 import org.apache.wicket.request.resource.AbstractResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public abstract class LogoutResource extends AbstractResource {
 
     private static final long serialVersionUID = 273797583932923564L;
 
+    protected static final Logger LOG = 
LoggerFactory.getLogger(LogoutResource.class);
+
     protected abstract Class<? extends WebPage> getLogoutPageClass();
 
     @Override
     protected ResourceResponse newResourceResponse(final Attributes 
attributes) {
-        throw new RestartResponseException(getLogoutPageClass(), new 
PageParameters());
+        HttpServletRequest request = (HttpServletRequest) 
attributes.getRequest().getContainerRequest();
+
+        // if no logout token was found, complete RP-initated logout
+        // otherwise, proceed with back-channel logout for the provided token
+        String logoutToken = 
Optional.ofNullable(request.getParameter(OIDCConstants.LOGOUT_TOKEN)).
+                orElseThrow(() -> new 
RestartResponseException(getLogoutPageClass(), new PageParameters()));
+
+        OIDCC4UIService service = 
BaseSession.class.cast(Session.get()).getAnonymousService(OIDCC4UIService.class);
+
+        ResourceResponse response = new ResourceResponse();
+        response.getHeaders().addHeader(HttpHeaders.CACHE_CONTROL, "no-cache, 
no-store");
+        response.getHeaders().addHeader("Pragma", "no-cache");
+        try {
+            service.backChannelLogout(logoutToken, 
request.getRequestURL().toString());
+
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("While requesting back-channel logout for token {}", 
logoutToken, e);
+
+            
response.setStatusCode(Response.Status.BAD_REQUEST.getStatusCode());
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setWriteCallback(new WriteCallback() {
+
+                @Override
+                public void writeData(final Attributes atrbts) throws 
IOException {
+                    atrbts.getResponse().write("{\"error\": \"" + 
e.getMessage() + "\"}");
+                }
+            });
+        }
+        return response;
     }
 }
diff --git 
a/ext/oidcc4ui/common-lib/src/main/java/org/apache/syncope/common/lib/oidc/OIDCConstants.java
 
b/ext/oidcc4ui/common-lib/src/main/java/org/apache/syncope/common/lib/oidc/OIDCConstants.java
index 7096ae1919..9de52e994b 100644
--- 
a/ext/oidcc4ui/common-lib/src/main/java/org/apache/syncope/common/lib/oidc/OIDCConstants.java
+++ 
b/ext/oidcc4ui/common-lib/src/main/java/org/apache/syncope/common/lib/oidc/OIDCConstants.java
@@ -26,6 +26,8 @@ public final class OIDCConstants {
 
     public static final String OP = "op";
 
+    public static final String LOGOUT_TOKEN = "logout_token";
+
     private OIDCConstants() {
         // private constructor for static utility class
     }
diff --git 
a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogic.java
 
b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogic.java
index 4af951e0b7..ba358ca0a3 100644
--- 
a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogic.java
+++ 
b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogic.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.logic;
 
+import com.nimbusds.jwt.JWT;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
 import java.lang.reflect.Method;
@@ -31,6 +32,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.oidc.OIDCConstants;
 import org.apache.syncope.common.lib.oidc.OIDCLoginResponse;
 import org.apache.syncope.common.lib.oidc.OIDCRequest;
 import org.apache.syncope.common.lib.to.EntityTO;
@@ -44,6 +46,7 @@ import org.apache.syncope.core.logic.oidc.OIDCC4UIContext;
 import org.apache.syncope.core.logic.oidc.OIDCClientCache;
 import org.apache.syncope.core.logic.oidc.OIDCUserManager;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
+import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.OIDCC4UIProviderDAO;
 import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProvider;
@@ -53,7 +56,10 @@ import 
org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.pac4j.core.context.CallContext;
 import org.pac4j.core.context.WebContext;
+import org.pac4j.core.credentials.Credentials;
+import org.pac4j.core.credentials.SessionKeyCredentials;
 import org.pac4j.core.exception.http.WithLocationAction;
+import org.pac4j.core.util.Pac4jConstants;
 import org.pac4j.oidc.client.OidcClient;
 import org.pac4j.oidc.config.OidcConfiguration;
 import org.pac4j.oidc.credentials.OidcCredentials;
@@ -77,6 +83,8 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
 
     protected final OIDCC4UIProviderDAO opDAO;
 
+    protected final AccessTokenDAO accessTokenDAO;
+
     protected final OIDCUserManager userManager;
 
     protected final EncryptorManager encryptorManager;
@@ -87,6 +95,7 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
             final AuthDataAccessor authDataAccessor,
             final AccessTokenDataBinder accessTokenDataBinder,
             final OIDCC4UIProviderDAO opDAO,
+            final AccessTokenDAO accessTokenDAO,
             final OIDCUserManager userManager,
             final EncryptorManager encryptorManager) {
 
@@ -95,6 +104,7 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
         this.authDataAccessor = authDataAccessor;
         this.accessTokenDataBinder = accessTokenDataBinder;
         this.opDAO = opDAO;
+        this.accessTokenDAO = accessTokenDAO;
         this.userManager = userManager;
         this.encryptorManager = encryptorManager;
     }
@@ -104,7 +114,14 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
             final OIDCC4UIProvider op,
             final String callbackUrl) {
 
-        return oidcClientCache.get(op.getName()).orElseGet(() -> 
oidcClientCache.add(op, callbackUrl));
+        return oidcClientCache.get(op.getName()).
+                map(oidcClient -> {
+                    Optional.ofNullable(callbackUrl).
+                            filter(c -> 
!c.equals(oidcClient.getCallbackUrl())).
+                            ifPresent(oidcClient::setCallbackUrl);
+                    return oidcClient;
+                }).
+                orElseGet(() -> oidcClientCache.add(op, callbackUrl));
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
@@ -163,8 +180,9 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
             oidcClient.getAuthenticator().validate(
                     new CallContext(new OIDCC4UIContext(), 
NoOpSessionStore.INSTANCE), credentials);
 
-            idToken = credentials.toIdToken().getJWTClaimsSet();
-            idTokenHint = credentials.toIdToken().serialize();
+            JWT jwt = credentials.toIdToken();
+            idToken = jwt.getJWTClaimsSet();
+            idTokenHint = jwt.serialize();
         } catch (Exception e) {
             LOG.error("While validating Token Response", e);
             SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.Unknown);
@@ -258,8 +276,12 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
             LOG.error("Could not fetch authorities", e);
         }
 
-        Pair<String, OffsetDateTime> accessTokenInfo =
-                accessTokenDataBinder.create(loginResp.getUsername(), claims, 
authorities, true);
+        Pair<String, OffsetDateTime> accessTokenInfo = 
accessTokenDataBinder.create(
+                
Optional.ofNullable(idToken.getClaim(Pac4jConstants.OIDC_CLAIM_SESSIONID)).map(Object::toString),
+                loginResp.getUsername(),
+                claims,
+                authorities,
+                true);
         loginResp.setAccessToken(accessTokenInfo.getLeft());
         loginResp.setAccessTokenExpiryTime(accessTokenInfo.getRight());
 
@@ -278,11 +300,11 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
             sce.getElements().add(e.getMessage());
             throw sce;
         }
+        String opName = (String) claimsSet.getClaim(JWT_CLAIM_OP_NAME);
 
         // 1. look for OidcClient
-        OIDCC4UIProvider op = opDAO.findByName((String) 
claimsSet.getClaim(JWT_CLAIM_OP_NAME)).
-                orElseThrow(() -> new NotFoundException(""
-                + "OIDC Provider '" + claimsSet.getClaim(JWT_CLAIM_OP_NAME) + 
'\''));
+        OIDCC4UIProvider op = opDAO.findByName(opName).
+                orElseThrow(() -> new NotFoundException("OIDC Provider '" + 
opName + '\''));
         OidcClient oidcClient = getOidcClient(oidcClientCacheLogout, op, 
redirectURI);
 
         // 2. create OIDCRequest
@@ -305,6 +327,45 @@ public class OIDCC4UILogic extends 
AbstractTransactionalLogic<EntityTO> {
         return logoutRequest;
     }
 
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public void backChannelLogout(final String logoutToken, final String 
redirectURI) {
+        // 0. parse the logout token to identify the OP
+        JWTClaimsSet claimsSet;
+        try {
+            SignedJWT jwt = SignedJWT.parse(logoutToken);
+            claimsSet = jwt.getJWTClaimsSet();
+        } catch (ParseException e) {
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.InvalidAccessToken);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        String opName = claimsSet.getAudience().getFirst();
+
+        // 1. look for OidcClient
+        OIDCC4UIProvider op = opDAO.findByName(opName).
+                orElseThrow(() -> new NotFoundException("OIDC Provider '" + 
opName + '\''));
+        OidcClient oidcClient = getOidcClient(oidcClientCacheLogout, op, 
redirectURI);
+
+        // 2. get the JWT key
+        Credentials credentials = oidcClient.getCredentials(new 
CallContext(new OIDCC4UIContext() {
+
+            @Override
+            public Optional<String> getRequestParameter(final String name) {
+                if (OIDCConstants.LOGOUT_TOKEN.equals(name)) {
+                    return Optional.of(logoutToken);
+                }
+                return Optional.empty();
+            }
+        }, NoOpSessionStore.INSTANCE)).orElseThrow(() -> {
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.Unknown);
+            sce.getElements().add("Could not validate the logout token");
+            return sce;
+        });
+
+        // 3. delete the JWT
+        accessTokenDAO.deleteById(((SessionKeyCredentials) 
credentials).getSessionKey());
+    }
+
     @Override
     protected EntityTO resolveReference(
             final Method method, final Object... args) throws 
UnresolvedReferenceException {
diff --git 
a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
 
b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
index 47f64971ea..c3c2e88318 100644
--- 
a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
+++ 
b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
@@ -22,6 +22,7 @@ import org.apache.syncope.core.logic.init.OIDCC4UILoader;
 import org.apache.syncope.core.logic.oidc.OIDCClientCache;
 import org.apache.syncope.core.logic.oidc.OIDCUserManager;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
+import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.dao.OIDCC4UIProviderDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
@@ -90,6 +91,7 @@ public class OIDCC4UILogicContext {
             final AuthDataAccessor authDataAccessor,
             final AccessTokenDataBinder accessTokenDataBinder,
             final OIDCC4UIProviderDAO opDAO,
+            final AccessTokenDAO accessTokenDAO,
             final OIDCUserManager userManager,
             final EncryptorManager encryptorManager) {
 
@@ -99,6 +101,7 @@ public class OIDCC4UILogicContext {
                 authDataAccessor,
                 accessTokenDataBinder,
                 opDAO,
+                accessTokenDAO,
                 userManager,
                 encryptorManager);
     }
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 9589575063..6064a67467 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
@@ -20,6 +20,7 @@ package org.apache.syncope.core.logic.oidc;
 
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.openid.connect.sdk.SubjectType;
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
@@ -39,6 +40,7 @@ import 
org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
 import org.pac4j.oidc.client.OidcClient;
 import org.pac4j.oidc.config.OidcConfiguration;
 import org.pac4j.oidc.metadata.StaticOidcOpMetadataResolver;
+import org.pac4j.oidc.profile.creator.TokenValidator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,15 +54,21 @@ public class OIDCClientCache {
     protected static final Function<String, String> DISCOVERY_URI =
             issuer -> issuer + "/.well-known/openid-configuration";
 
-    public static void importMetadata(final OIDCC4UIProviderTO opTO)
+    protected static OIDCProviderMetadata fetchMetadata(final String issuer)
             throws IOException, InterruptedException, ParseException {
 
-        String discoveryDocumentURI = DISCOVERY_URI.apply(opTO.getIssuer());
+        String discoveryDocumentURI = DISCOVERY_URI.apply(issuer);
         HttpResponse<String> response = HttpClient.newBuilder().build().send(
                 
HttpRequest.newBuilder(URI.create(discoveryDocumentURI)).GET().build(),
                 HttpResponse.BodyHandlers.ofString());
 
-        OIDCProviderMetadata metadata = 
OIDCProviderMetadata.parse(response.body());
+        return OIDCProviderMetadata.parse(response.body());
+    }
+
+    public static void importMetadata(final OIDCC4UIProviderTO opTO)
+            throws IOException, InterruptedException, ParseException {
+
+        OIDCProviderMetadata metadata = fetchMetadata(opTO.getIssuer());
 
         opTO.setIssuer(
                 
Optional.ofNullable(metadata.getIssuer()).map(Issuer::getValue).orElse(null));
@@ -84,11 +92,16 @@ public class OIDCClientCache {
     }
 
     public OidcClient add(final OIDCC4UIProvider op, final String callbackUrl) 
{
+        OidcConfiguration cfg = new OidcConfiguration();
+        cfg.setClientId(op.getClientID());
+        cfg.setSecret(op.getClientSecret());
+        cfg.setScope(String.join(" ", op.getScopes()));
+        cfg.setUseNonce(false);
+
         OIDCProviderMetadata metadata = new OIDCProviderMetadata(
                 new Issuer(op.getIssuer()),
                 List.of(SubjectType.PUBLIC),
                 
Optional.ofNullable(op.getJwksUri()).map(URI::create).orElse(null));
-        metadata.setIDTokenJWSAlgs(List.of(JWSAlgorithm.HS256));
         metadata.setAuthorizationEndpointURI(
                 
Optional.ofNullable(op.getAuthorizationEndpoint()).map(URI::create).orElse(null));
         metadata.setTokenEndpointURI(
@@ -97,15 +110,37 @@ public class OIDCClientCache {
                 
Optional.ofNullable(op.getUserinfoEndpoint()).map(URI::create).orElse(null));
         metadata.setEndSessionEndpointURI(
                 
Optional.ofNullable(op.getEndSessionEndpoint()).map(URI::create).orElse(null));
+        if (op.getHasDiscovery()) {
+            try {
+                
metadata.setIDTokenJWSAlgs(fetchMetadata(op.getIssuer()).getIDTokenJWSAlgs());
+            } catch (Exception e) {
+                LOG.error("While fetching OIDC metadata for issuer {}", 
op.getIssuer(), e);
+                metadata.setIDTokenJWSAlgs(List.of(JWSAlgorithm.HS256));
+            }
+        }
+        cfg.setOpMetadataResolver(new StaticOidcOpMetadataResolver(cfg, 
metadata) {
 
-        OidcConfiguration cfg = new OidcConfiguration();
-        cfg.setClientId(op.getClientID());
-        cfg.setSecret(op.getClientSecret());
-        cfg.setDiscoveryURI(DISCOVERY_URI.apply(op.getIssuer()));
-        cfg.setPreferredJwsAlgorithm(JWSAlgorithm.HS256);
-        cfg.setOpMetadataResolver(new StaticOidcOpMetadataResolver(cfg, 
metadata));
-        cfg.setScope(String.join(" ", op.getScopes()));
-        cfg.setUseNonce(false);
+            @Override
+            public boolean hasChanged() {
+                return true;
+            }
+
+            @Override
+            public ClientAuthentication getClientAuthentication() {
+                if (clientAuthentication == null) {
+                    clientAuthentication = computeClientAuthentication();
+                }
+                return clientAuthentication;
+            }
+
+            @Override
+            public TokenValidator getTokenValidator() {
+                if (tokenValidator == null) {
+                    tokenValidator = new TokenValidator(configuration, 
metadata);
+                }
+                return tokenValidator;
+            }
+        });
 
         OidcClient client = new OidcClient(cfg);
         client.setName(op.getName());
diff --git 
a/ext/oidcc4ui/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCC4UIService.java
 
b/ext/oidcc4ui/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCC4UIService.java
index 0afb715764..43b994b443 100644
--- 
a/ext/oidcc4ui/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCC4UIService.java
+++ 
b/ext/oidcc4ui/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/OIDCC4UIService.java
@@ -85,4 +85,17 @@ public interface OIDCC4UIService extends JAXRSService {
     @Path("logout")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
     OIDCRequest createLogoutRequest(@QueryParam(OIDCConstants.REDIRECT_URI) 
String redirectURI);
+
+    /**
+     * Removes the JWT matching the provided OIDC logout token.
+     *
+     * @param logoutToken logout token
+     * @param redirectURI redirect URI
+     */
+    @POST
+    @Path("backChannelLogout")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    void backChannelLogout(
+            @QueryParam(OIDCConstants.LOGOUT_TOKEN) String logoutToken,
+            @QueryParam(OIDCConstants.REDIRECT_URI) String redirectURI);
 }
diff --git 
a/ext/oidcc4ui/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCC4UIServiceImpl.java
 
b/ext/oidcc4ui/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCC4UIServiceImpl.java
index a27beca5c7..88f1946700 100644
--- 
a/ext/oidcc4ui/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCC4UIServiceImpl.java
+++ 
b/ext/oidcc4ui/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/OIDCC4UIServiceImpl.java
@@ -45,16 +45,17 @@ public class OIDCC4UIServiceImpl extends AbstractService 
implements OIDCC4UIServ
 
     @Override
     public OIDCRequest createLogoutRequest(final String redirectURI) {
-        return logic.createLogoutRequest(getAccessToken(), redirectURI);
-    }
-
-    private String getAccessToken() {
         String auth = 
messageContext.getHttpHeaders().getHeaderString(HttpHeaders.AUTHORIZATION);
         String[] parts = Optional.ofNullable(auth).map(s -> s.split(" 
")).orElse(null);
         if (parts == null || parts.length != 2 || !"Bearer".equals(parts[0])) {
             return null;
         }
 
-        return parts[1];
+        return logic.createLogoutRequest(parts[1], redirectURI);
+    }
+
+    @Override
+    public void backChannelLogout(final String logoutToken, final String 
redirectURI) {
+        logic.backChannelLogout(logoutToken, redirectURI);
     }
 }
diff --git 
a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogic.java
 
b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogic.java
index 0944df22d6..4be70ffc8a 100644
--- 
a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogic.java
+++ 
b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogic.java
@@ -446,8 +446,8 @@ public class SAML2SP4UILogic extends 
AbstractSAML2SP4UILogic {
             LOG.error("Could not fetch authorities", e);
         }
 
-        Pair<String, OffsetDateTime> accessTokenInfo =
-                accessTokenDataBinder.create(loginResp.getUsername(), claims, 
authorities, true);
+        Pair<String, OffsetDateTime> accessTokenInfo = 
accessTokenDataBinder.create(
+                Optional.of(loginResp.getSessionIndex()), 
loginResp.getUsername(), claims, authorities, true);
         loginResp.setAccessToken(accessTokenInfo.getLeft());
         loginResp.setAccessTokenExpiryTime(accessTokenInfo.getRight());
 
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 01403501b3..cf33cd836d 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
@@ -56,6 +56,7 @@ import org.apache.syncope.common.lib.to.OIDCC4UIProviderTO;
 import org.apache.syncope.common.lib.to.OIDCRPClientAppTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientAppType;
+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;
@@ -95,6 +96,7 @@ public class OIDCC4UIITCase extends AbstractUIITCase {
         clientApp.getRedirectUris().add(baseAddress + 
OIDCC4UIConstants.URL_CONTEXT + "/code-consumer");
         clientApp.setSignIdToken(true);
         clientApp.setJwtAccessToken(true);
+        clientApp.setLogoutType(LogoutType.BACK_CHANNEL);
         clientApp.setLogoutUri(baseAddress + OIDCC4UIConstants.URL_CONTEXT + 
"/logout");
         clientApp.getSupportedResponseTypes().addAll(
                 Set.of(OIDCResponseType.CODE, OIDCResponseType.ID_TOKEN_TOKEN, 
OIDCResponseType.TOKEN));
@@ -156,11 +158,7 @@ public class OIDCC4UIITCase extends AbstractUIITCase {
             cas.setClientSecret(appName);
 
             cas.setIssuer(WA_ADDRESS + "/oidc");
-            cas.setAuthorizationEndpoint(cas.getIssuer() + "/authorize");
-            cas.setTokenEndpoint(cas.getIssuer() + "/accessToken");
-            cas.setJwksUri(cas.getIssuer() + "/jwks");
-            cas.setUserinfoEndpoint(cas.getIssuer() + "/profile");
-            cas.setEndSessionEndpoint(cas.getIssuer() + "/logout");
+            cas.setHasDiscovery(true);
 
             cas.getScopes().addAll(OIDCScopeConstants.ALL_STANDARD_SCOPES);
             cas.getScopes().add("syncope");
@@ -199,7 +197,7 @@ public class OIDCC4UIITCase extends AbstractUIITCase {
             item.setExtAttrName("name");
             cas.add(item);
 
-            OIDCC4UI_PROVIDER_SERVICE.create(cas);
+            OIDCC4UI_PROVIDER_SERVICE.createFromDiscovery(cas);
         }
     }
 
diff --git a/pom.xml b/pom.xml
index d8b2873915..e9af48a506 100644
--- a/pom.xml
+++ b/pom.xml
@@ -517,9 +517,9 @@ under the License.
     <docker.neo4j.version>5.26</docker.neo4j.version>
 
     <jdbc.postgresql.version>42.7.7</jdbc.postgresql.version>
-    <jdbc.mysql.version>9.2.0</jdbc.mysql.version>
+    <jdbc.mysql.version>9.4.0</jdbc.mysql.version>
     <jdbc.mariadb.version>3.5.4</jdbc.mariadb.version>
-    <jdbc.oracle.version>23.8.0.25.04</jdbc.oracle.version>
+    <jdbc.oracle.version>23.9.0.25.07</jdbc.oracle.version>
 
     <bundles.directory>${project.build.directory}/bundles</bundles.directory>
 
diff --git a/wa/starter/src/main/resources/wa.properties 
b/wa/starter/src/main/resources/wa.properties
index 8c2951a789..da54a7d426 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -51,6 +51,8 @@ cas.service-registry.schedule.start-delay=PT30S
 
 cas.events.core.enabled=false
 
+cas.slo.disabled=false
+
 spring.main.allow-bean-definition-overriding=true
 spring.main.lazy-initialization=false
 


Reply via email to