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