This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch 3_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/3_0_X by this push:
new 9e411ebf5f [SYNCOPE-1900] Supporting OIDC Back-Channel Logout for
Console and Enduser (#1148)
9e411ebf5f is described below
commit 9e411ebf5f5676602dcafe4a325cf403b7bada7c
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 | 4 +-
.../api/data/AccessTokenDataBinder.java | 3 +-
.../java/data/AccessTokenDataBinderImpl.java | 4 +-
.../spring/security/SyncopeJWTSSOProvider.java | 28 ++++++------
.../commons/resources/oidcc4ui/LogoutResource.java | 45 +++++++++++++++++-
.../syncope/common/lib/oidc/OIDCConstants.java | 2 +
.../apache/syncope/core/logic/OIDCC4UILogic.java | 53 ++++++++++++++++++----
.../syncope/core/logic/OIDCC4UILogicContext.java | 3 ++
.../syncope/core/logic/oidc/OIDCClientCache.java | 35 +++++++++-----
.../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 | 5 +-
15 files changed, 170 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 936c13a3ca..56d2063e2e 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
@@ -22,6 +22,7 @@ import java.lang.reflect.Method;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.SyncopeClientException;
@@ -82,8 +83,9 @@ public class AccessTokenLogic extends
AbstractTransactionalLogic<AccessTokenTO>
}
return binder.create(
+ Optional.empty(),
AuthContextUtils.getUsername(),
- Collections.<String, Object>emptyMap(),
+ Collections.emptyMap(),
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 053b4ffb0f..c1503c5a8e 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,
@@ -139,7 +141,7 @@ public class AccessTokenDataBinderImpl implements
AccessTokenDataBinder {
if (accessToken == null) {
// no AccessToken found: create new
accessToken = entityFactory.newEntity(AccessToken.class);
-
accessToken.setKey(SecureRandomUtils.generateRandomUUID().toString());
+ accessToken.setKey(key.orElseGet(() ->
SecureRandomUtils.generateRandomUUID().toString()));
accessToken = replace(subject, claims, authorities, accessToken);
} else if (replace
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 ecf4fc431a..c77123bccf 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
@@ -25,6 +25,7 @@ import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.jca.JCAContext;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
+import java.util.Optional;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.types.CipherAlgorithm;
@@ -36,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;
/**
@@ -94,22 +96,22 @@ public class SyncopeJWTSSOProvider implements
JWTSSOProvider {
@Transactional(readOnly = true)
@Override
public Pair<User, Set<SyncopeGrantedAuthority>> resolve(final JWTClaimsSet
jwtClaims) {
- User user = userDAO.findByUsername(jwtClaims.getSubject());
+ AccessToken accessToken =
Optional.ofNullable(accessTokenDAO.find(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.find(jwtClaims.getJWTID());
- if (accessToken != null && accessToken.getAuthorities() != null) {
- try {
- authorities = POJOHelper.deserialize(
- ENCRYPTOR.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(
+ ENCRYPTOR.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()),
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..8fa8c7177f 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 java.io.IOException;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+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 5b2ad68e15..cf3f452669 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 com.nimbusds.oauth2.sdk.AuthorizationCode;
@@ -44,6 +45,7 @@ import org.apache.syncope.core.logic.oidc.NoOpSessionStore;
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.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;
@@ -54,6 +56,7 @@ import
org.apache.syncope.core.spring.security.AuthDataAccessor;
import org.apache.syncope.core.spring.security.Encryptor;
import org.pac4j.core.context.WebContext;
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;
@@ -79,6 +82,8 @@ public class OIDCC4UILogic extends
AbstractTransactionalLogic<EntityTO> {
protected final OIDCC4UIProviderDAO opDAO;
+ protected final AccessTokenDAO accessTokenDAO;
+
protected final OIDCUserManager userManager;
public OIDCC4UILogic(
@@ -87,6 +92,7 @@ public class OIDCC4UILogic extends
AbstractTransactionalLogic<EntityTO> {
final AuthDataAccessor authDataAccessor,
final AccessTokenDataBinder accessTokenDataBinder,
final OIDCC4UIProviderDAO opDAO,
+ final AccessTokenDAO accessTokenDAO,
final OIDCUserManager userManager) {
this.oidcClientCacheLogin = oidcClientCacheLogin;
@@ -94,6 +100,7 @@ public class OIDCC4UILogic extends
AbstractTransactionalLogic<EntityTO> {
this.authDataAccessor = authDataAccessor;
this.accessTokenDataBinder = accessTokenDataBinder;
this.opDAO = opDAO;
+ this.accessTokenDAO = accessTokenDAO;
this.userManager = userManager;
}
@@ -102,7 +109,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 + "')")
@@ -159,8 +173,9 @@ public class OIDCC4UILogic extends
AbstractTransactionalLogic<EntityTO> {
oidcClient.getAuthenticator().validate(credentials, new
OIDCC4UIContext(), NoOpSessionStore.INSTANCE);
- idToken = credentials.getIdToken().getJWTClaimsSet();
- idTokenHint = credentials.getIdToken().serialize();
+ JWT jwt = credentials.getIdToken();
+ idToken = jwt.getJWTClaimsSet();
+ idTokenHint = jwt.serialize();
} catch (Exception e) {
LOG.error("While validating Token Response", e);
SyncopeClientException sce =
SyncopeClientException.build(ClientExceptionType.Unknown);
@@ -254,8 +269,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());
@@ -274,11 +293,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 = Optional.ofNullable(opDAO.findByName((String)
claimsSet.getClaim(JWT_CLAIM_OP_NAME))).
- orElseThrow(() -> new NotFoundException(""
- + "OIDC Provider '" + claimsSet.getClaim(JWT_CLAIM_OP_NAME) +
'\''));
+ OIDCC4UIProvider op = Optional.ofNullable(opDAO.findByName(opName)).
+ orElseThrow(() -> new NotFoundException("OIDC Provider '" +
opName + '\''));
OidcClient oidcClient = getOidcClient(oidcClientCacheLogout, op,
redirectURI);
// 2. create OIDCRequest
@@ -302,6 +321,24 @@ 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
+ 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;
+ }
+
+ // 1. delete the JWT
+
Optional.ofNullable(claimsSet.getClaim(Pac4jConstants.OIDC_CLAIM_SESSIONID)).
+ map(String.class::cast).ifPresent(accessTokenDAO::delete);
+ }
+
@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 a17de0b364..5b1bb6a584 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
@@ -21,6 +21,7 @@ package org.apache.syncope.core.logic;
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.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;
@@ -89,6 +90,7 @@ public class OIDCC4UILogicContext {
final AuthDataAccessor authDataAccessor,
final AccessTokenDataBinder accessTokenDataBinder,
final OIDCC4UIProviderDAO opDAO,
+ final AccessTokenDAO accessTokenDAO,
final OIDCUserManager userManager) {
return new OIDCC4UILogic(
@@ -97,6 +99,7 @@ public class OIDCC4UILogicContext {
authDataAccessor,
accessTokenDataBinder,
opDAO,
+ accessTokenDAO,
userManager);
}
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 dbda7839b6..503860d17c 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
@@ -18,6 +18,7 @@
*/
package org.apache.syncope.core.logic.oidc;
+import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.SubjectType;
@@ -32,7 +33,6 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
-import java.util.stream.Collectors;
import org.apache.syncope.common.lib.to.OIDCC4UIProviderTO;
import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProvider;
import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver;
@@ -51,15 +51,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));
@@ -83,6 +89,12 @@ 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),
@@ -95,14 +107,15 @@ public class OIDCClientCache {
Optional.ofNullable(op.getUserinfoEndpoint()).map(URI::create).orElse(null));
metadata.setEndSessionEndpointURI(
Optional.ofNullable(op.getEndSessionEndpoint()).map(URI::create).orElse(null));
-
- OidcConfiguration cfg = new OidcConfiguration();
- cfg.setClientId(op.getClientID());
- cfg.setSecret(op.getClientSecret());
+ 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.setProviderMetadata(metadata);
- cfg.setScope(op.getScopes().stream().collect(Collectors.joining(" ")));
- cfg.setUseNonce(false);
- cfg.setLogoutHandler(new NoOpLogoutHandler());
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 19f8ec371f..d658df5862 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 f116fbd381..250aa316be 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
@@ -47,16 +47,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 8152c8deb0..4704420504 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
@@ -439,8 +439,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 831703254f..52171903fc 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
@@ -51,6 +51,7 @@ import org.apache.syncope.common.lib.to.Item;
import org.apache.syncope.common.lib.to.OIDCC4UIProviderTO;
import org.apache.syncope.common.lib.to.OIDCRPClientAppTO;
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;
@@ -90,6 +91,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));
@@ -134,11 +136,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");
@@ -177,7 +175,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 c2edcfecb0..3391412a24 100644
--- a/pom.xml
+++ b/pom.xml
@@ -508,10 +508,10 @@ under the License.
<docker.mariadb.version>11</docker.mariadb.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.mssql.version>12.6.1.jre11</jdbc.mssql.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 ea2a6ee0cd..ff887f8c84 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -50,9 +50,8 @@ cas.service-registry.schedule.start-delay=PT30S
cas.events.core.enabled=false
-##
-# Allow configuration classes to override bean definitions from Spring Boot
-#
+cas.slo.disabled=false
+
spring.main.allow-bean-definition-overriding=true
spring.main.lazy-initialization=false