This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 51cf43262f79775228a439ae336c04598269af04 Author: Francesco Chicchiriccò <ilgro...@apache.org> AuthorDate: Fri Jan 27 11:09:44 2023 +0100 Upgrading pac4j --- .../console/src/main/resources/console.properties | 1 + .../src/test/resources/console-debug.properties | 4 +- .../enduser/src/main/resources/enduser.properties | 1 + core/starter/src/main/resources/core.properties | 1 + .../apache/syncope/core/logic/OIDCC4UILogic.java | 5 +- .../syncope/core/logic/oidc/OIDCClientCache.java | 31 +++--- .../apache/syncope/core/logic/SAML2SP4UILogic.java | 104 +++++++++++---------- .../syncope/core/logic/saml2/SAML2ClientCache.java | 15 ++- .../core/logic/saml2/SAML2SP4UIContext.java | 18 ++-- fit/wa-reference/src/main/resources/log4j2.xml | 3 + .../org/apache/syncope/fit/sra/SAML2SRAITCase.java | 3 +- .../apache/syncope/fit/ui/SAML2SP4UIITCase.java | 3 +- pom.xml | 6 +- .../org/apache/syncope/sra/SecurityConfig.java | 15 ++- .../sra/security/pac4j/RedirectionActionUtils.java | 18 ++-- .../security/saml2/SAML2AuthenticationToken.java | 15 +-- .../saml2/SAML2LogoutResponseWebFilter.java | 79 +++------------- .../saml2/SAML2RequestServerLogoutHandler.java | 10 +- .../security/saml2/SAML2SecurityConfigUtils.java | 2 +- .../SAML2WebSsoAuthenticationRequestWebFilter.java | 17 +++- .../saml2/SAML2WebSsoAuthenticationWebFilter.java | 30 +++--- sra/src/main/resources/sra.properties | 1 + .../syncope/wa/starter/config/WAContext.java | 80 ++++++++++------ .../saml/idp/WASamlIdPCasEventListener.java | 26 ++---- ...erator.java => WASamlIdPMetadataGenerator.java} | 42 ++++++--- ...aLocator.java => WASamlIdPMetadataLocator.java} | 25 +++-- .../wa/starter/services/WAServiceRegistry.java | 20 ++-- wa/starter/src/main/resources/wa.properties | 1 + 28 files changed, 295 insertions(+), 281 deletions(-) diff --git a/client/idrepo/console/src/main/resources/console.properties b/client/idrepo/console/src/main/resources/console.properties index 44dbf8186d..173d5e5aa4 100644 --- a/client/idrepo/console/src/main/resources/console.properties +++ b/client/idrepo/console/src/main/resources/console.properties @@ -27,6 +27,7 @@ server.servlet.contextPath=/syncope-console management.endpoints.web.exposure.include=info,health,loggers management.endpoint.health.show-details=ALWAYS +management.endpoint.env.show-values=WHEN_AUTHORIZED service.discovery.address=http://localhost:8080/syncope-console/ diff --git a/client/idrepo/console/src/test/resources/console-debug.properties b/client/idrepo/console/src/test/resources/console-debug.properties index 8472125348..d41b7337e0 100644 --- a/client/idrepo/console/src/test/resources/console-debug.properties +++ b/client/idrepo/console/src/test/resources/console-debug.properties @@ -14,8 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -keymaster.address=http://localhost:9080/syncope/rest/keymaster -#keymaster.address=https://localhost:9443/syncope/rest/keymaster +#keymaster.address=http://localhost:9080/syncope/rest/keymaster +keymaster.address=https://localhost:9443/syncope/rest/keymaster keymaster.username=${anonymousUser} keymaster.password=${anonymousKey} diff --git a/client/idrepo/enduser/src/main/resources/enduser.properties b/client/idrepo/enduser/src/main/resources/enduser.properties index cf0af9ccc8..c7296757c3 100644 --- a/client/idrepo/enduser/src/main/resources/enduser.properties +++ b/client/idrepo/enduser/src/main/resources/enduser.properties @@ -27,6 +27,7 @@ server.servlet.contextPath=/syncope-enduser management.endpoints.web.exposure.include=info,health,loggers management.endpoint.health.show-details=ALWAYS +management.endpoint.env.show-values=WHEN_AUTHORIZED service.discovery.address=http://localhost:8080/syncope-enduser/ diff --git a/core/starter/src/main/resources/core.properties b/core/starter/src/main/resources/core.properties index 933aed4181..f75601eef6 100644 --- a/core/starter/src/main/resources/core.properties +++ b/core/starter/src/main/resources/core.properties @@ -29,6 +29,7 @@ cxf.path=/rest management.endpoints.web.exposure.include=health,info,loggers,entityCache management.endpoint.health.show-details=ALWAYS +management.endpoint.env.show-values=WHEN_AUTHORIZED service.discovery.address=http://localhost:8080/syncope/rest/ 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 24fe4b0ee7..d3b5fb72bc 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 @@ -54,7 +54,6 @@ import org.apache.syncope.core.spring.security.AuthDataAccessor; import org.apache.syncope.core.spring.security.Encryptor; import org.pac4j.core.context.CallContext; import org.pac4j.core.exception.http.WithLocationAction; -import org.pac4j.core.profile.factory.ProfileManagerFactory; import org.pac4j.oidc.client.OidcClient; import org.pac4j.oidc.credentials.OidcCredentials; import org.pac4j.oidc.profile.OidcProfile; @@ -113,7 +112,7 @@ public class OIDCC4UILogic extends AbstractTransactionalLogic<EntityTO> { // 2. create OIDCRequest WithLocationAction action = oidcClient.getRedirectionAction( - new CallContext(new OIDC4UIContext(), NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)). + new CallContext(new OIDC4UIContext(), NoOpSessionStore.INSTANCE)). map(WithLocationAction.class::cast). orElseThrow(() -> { SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); @@ -275,7 +274,7 @@ public class OIDCC4UILogic extends AbstractTransactionalLogic<EntityTO> { profile.setIdTokenString((String) claimsSet.getClaim(JWT_CLAIM_ID_TOKEN)); WithLocationAction action = oidcClient.getLogoutAction( - new CallContext(new OIDC4UIContext(), NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT), + new CallContext(new OIDC4UIContext(), NoOpSessionStore.INSTANCE), profile, redirectURI). map(WithLocationAction.class::cast). 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 4304e677dd..19ea7c3ab6 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; @@ -31,6 +32,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Function; import org.apache.syncope.common.lib.SyncopeClientException; import org.apache.syncope.common.lib.to.OIDCC4UIProviderTO; import org.apache.syncope.common.lib.types.ClientExceptionType; @@ -38,6 +40,7 @@ import org.apache.syncope.core.persistence.api.entity.OIDCC4UIProvider; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,18 +51,21 @@ public class OIDCClientCache { protected static final Logger LOG = LoggerFactory.getLogger(OIDCClientCache.class); + protected static final Function<String, String> DISCOVERY_URI = + issuer -> issuer + "/.well-known/openid-configuration"; + protected final List<OidcClient> cache = Collections.synchronizedList(new ArrayList<>()); protected static OIDCProviderMetadata getDiscoveryDocument(final String issuer) { - String discoveryDocumentURL = issuer + "/.well-known/openid-configuration"; + String discoveryDocumentURI = DISCOVERY_URI.apply(issuer); try { HttpResponse<String> response = HttpClient.newBuilder().build().send( - HttpRequest.newBuilder(URI.create(discoveryDocumentURL)).GET().build(), + HttpRequest.newBuilder(URI.create(discoveryDocumentURI)).GET().build(), HttpResponse.BodyHandlers.ofString()); return OIDCProviderMetadata.parse(response.body()); } catch (IOException | InterruptedException | ParseException e) { - LOG.error("While getting the Discovery Document at {}", discoveryDocumentURL, e); + LOG.error("While getting the Discovery Document at {}", discoveryDocumentURI, e); SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); sce.getElements().add(e.getMessage()); @@ -93,6 +99,7 @@ public class OIDCClientCache { 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( @@ -102,15 +109,17 @@ public class OIDCClientCache { metadata.setEndSessionEndpointURI( Optional.ofNullable(op.getEndSessionEndpoint()).map(URI::create).orElse(null)); - OidcConfiguration config = new OidcConfiguration(); - config.setClientId(op.getClientID()); - config.setSecret(op.getClientSecret()); - config.setProviderMetadata(metadata); - config.setScope("openid profile email address phone offline_access"); - config.setUseNonce(false); - config.setSessionLogoutHandler(new NoOpSessionLogoutHandler()); + 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("openid profile email address phone offline_access"); + cfg.setUseNonce(false); + cfg.setSessionLogoutHandler(new NoOpSessionLogoutHandler()); - OidcClient client = new OidcClient(config); + OidcClient client = new OidcClient(cfg); client.setName(op.getName()); client.setCallbackUrlResolver(new NoParameterCallbackUrlResolver()); client.setCallbackUrl(callbackUrl); 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 264574b521..55d1d9598a 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 @@ -72,14 +72,15 @@ import org.opensaml.saml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder; import org.pac4j.core.context.CallContext; +import org.pac4j.core.credentials.Credentials; import org.pac4j.core.exception.http.RedirectionAction; import org.pac4j.core.exception.http.WithContentAction; import org.pac4j.core.exception.http.WithLocationAction; import org.pac4j.core.logout.NoLogoutActionBuilder; -import org.pac4j.core.profile.factory.ProfileManagerFactory; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; import org.pac4j.saml.context.SAML2MessageContext; +import org.pac4j.saml.credentials.SAML2AuthenticationCredentials; import org.pac4j.saml.credentials.SAML2Credentials; import org.pac4j.saml.credentials.authenticator.SAML2Authenticator; import org.pac4j.saml.metadata.SAML2ServiceProviderMetadataResolver; @@ -102,6 +103,28 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { protected static final Encryptor ENCRYPTOR = Encryptor.getInstance(); + protected static String validateUrl(final String url) { + boolean isValid = true; + if (url.contains("..")) { + isValid = false; + } + if (isValid) { + isValid = ResourceUtils.isUrl(url); + } + + if (!isValid) { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); + sce.getElements().add("Invalid URL: " + url); + throw sce; + } + + return url; + } + + protected static String getCallbackUrl(final String spEntityID, final String urlContext) { + return validateUrl(spEntityID + urlContext + "/assertion-consumer"); + } + protected final SAML2SP4UILoader loader; protected final AccessTokenDataBinder accessTokenDataBinder; @@ -134,28 +157,6 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { this.authDataAccessor = authDataAccessor; } - protected static String validateUrl(final String url) { - boolean isValid = true; - if (url.contains("..")) { - isValid = false; - } - if (isValid) { - isValid = ResourceUtils.isUrl(url); - } - - if (!isValid) { - SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); - sce.getElements().add("Invalid URL: " + url); - throw sce; - } - - return url; - } - - protected static String getCallbackUrl(final String spEntityID, final String urlContext) { - return validateUrl(spEntityID + urlContext + "/assertion-consumer"); - } - @PreAuthorize("isAuthenticated()") public void getMetadata(final String spEntityID, final String urlContext, final OutputStream os) { String metadata = metadataCache.get(spEntityID + urlContext); @@ -221,17 +222,13 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { return getSAML2Client(idp, spEntityID, urlContext); } - protected static SAML2Request buildRequest(final String idpEntityID, final RedirectionAction action) { + protected SAML2Request buildRequest(final String idpEntityID, final RedirectionAction action) { SAML2Request requestTO = new SAML2Request(); requestTO.setIdpEntityID(idpEntityID); - if (action instanceof WithLocationAction) { - WithLocationAction withLocationAction = (WithLocationAction) action; - + if (action instanceof WithLocationAction withLocationAction) { requestTO.setBindingType(SAML2BindingType.REDIRECT); requestTO.setContent(withLocationAction.getLocation()); - } else if (action instanceof WithContentAction) { - WithContentAction withContentAction = (WithContentAction) action; - + } else if (action instanceof WithContentAction withContentAction) { requestTO.setBindingType(SAML2BindingType.POST); requestTO.setContent(Base64.getMimeEncoder().encodeToString(withContentAction.getContent().getBytes())); } @@ -294,7 +291,7 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { saml2Client.getConfiguration().getAuthnRequestBindingType(), null); RedirectionAction action = saml2Client.getRedirectionAction( - new CallContext(ctx, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)). + new CallContext(ctx, NoOpSessionStore.INSTANCE)). orElseThrow(() -> { SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); sce.getElements().add("No RedirectionAction generated for AuthnRequest"); @@ -315,17 +312,20 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { SAML2Client saml2Client = getSAML2Client(idp, saml2Response.getSpEntityID(), saml2Response.getUrlContext()); // 2. validate the provided SAML response - SAML2Credentials credentials; - try { - SAML2SP4UIContext ctx = new SAML2SP4UIContext( - saml2Client.getConfiguration().getAuthnRequestBindingType(), - saml2Response); + SAML2SP4UIContext webCtx = new SAML2SP4UIContext( + saml2Client.getConfiguration().getAuthnRequestBindingType(), + saml2Response); + CallContext ctx = new CallContext(webCtx, NoOpSessionStore.INSTANCE); - credentials = (SAML2Credentials) saml2Client.getCredentialsExtractor(). - extract(new CallContext(ctx, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)). - orElseThrow(() -> new IllegalStateException("No AuthnResponse found")); + SAML2AuthenticationCredentials authCreds; + try { + Credentials creds = saml2Client.getCredentialsExtractor(). + extract(ctx). + orElseThrow(() -> new IllegalStateException("Could not extract credentials")); - saml2Client.getAuthenticator().validate(new CallContext(ctx, NoOpSessionStore.INSTANCE), credentials); + authCreds = saml2Client.validateCredentials(ctx, creds). + map(SAML2AuthenticationCredentials.class::cast). + orElseThrow(() -> new IllegalArgumentException("Invalid SAML credentials provided")); } catch (Exception e) { LOG.error("While validating AuthnResponse", e); SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown); @@ -338,7 +338,7 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { loginResp.setIdp(saml2Client.getIdentityProviderResolvedEntityId()); loginResp.setSloSupported(!(saml2Client.getLogoutActionBuilder() instanceof NoLogoutActionBuilder)); - SAML2Credentials.SAMLNameID nameID = credentials.getNameId(); + SAML2AuthenticationCredentials.SAMLNameID nameID = authCreds.getNameId(); Item connObjectKeyItem = idp.getConnObjectKeyItem().orElse(null); @@ -350,11 +350,11 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { keyValue = nameID.getValue(); } - loginResp.setNotOnOrAfter(new Date(credentials.getConditions().getNotOnOrAfter().toInstant().toEpochMilli())); + loginResp.setNotOnOrAfter(new Date(authCreds.getConditions().getNotOnOrAfter().toInstant().toEpochMilli())); - loginResp.setSessionIndex(credentials.getSessionIndex()); + loginResp.setSessionIndex(authCreds.getSessionIndex()); - for (SAML2Credentials.SAMLAttribute attr : credentials.getAttributes()) { + for (SAML2AuthenticationCredentials.SAMLAttribute attr : authCreds.getAttributes()) { if (!attr.getAttributeValues().isEmpty()) { String attrName = attr.getFriendlyName() == null ? attr.getName() : attr.getFriendlyName(); if (connObjectKeyItem != null && attrName.equals(connObjectKeyItem.getExtAttrName())) { @@ -477,7 +477,7 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { SAML2SP4UIContext ctx = new SAML2SP4UIContext( saml2Client.getConfiguration().getSpLogoutRequestBindingType(), null); RedirectionAction action = saml2Client.getLogoutAction( - new CallContext(ctx, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT), + new CallContext(ctx, NoOpSessionStore.INSTANCE), saml2Profile, null). orElseThrow(() -> { @@ -504,17 +504,19 @@ public class SAML2SP4UILogic extends AbstractTransactionalLogic<EntityTO> { } // 2. validate the provided SAML response - SAML2SP4UIContext ctx = new SAML2SP4UIContext( - saml2Client.getConfiguration().getSpLogoutRequestBindingType(), + SAML2SP4UIContext webCtx = new SAML2SP4UIContext( + saml2Client.getConfiguration().getAuthnRequestBindingType(), saml2Response); + CallContext ctx = new CallContext(webCtx, NoOpSessionStore.INSTANCE); LogoutResponse logoutResponse; try { - SAML2MessageContext saml2Ctx = saml2Client.getContextProvider().buildContext( - new CallContext(ctx, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT), saml2Client); - saml2Client.getLogoutProfileHandler().receive(saml2Ctx); + Credentials creds = saml2Client.getCredentialsExtractor(). + extract(ctx). + orElseThrow(() -> new IllegalStateException("Could not extract credentials")); - logoutResponse = (LogoutResponse) saml2Ctx.getMessageContext().getMessage(); + saml2Client.getLogoutProcessor().processLogout(ctx, creds); + logoutResponse = (LogoutResponse) ((SAML2Credentials) creds).getContext().getMessageContext().getMessage(); } catch (Exception e) { LOG.error("Could not validate LogoutResponse", e); return; diff --git a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ClientCache.java b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ClientCache.java index 945d2587f6..15577bcc34 100644 --- a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ClientCache.java +++ b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ClientCache.java @@ -27,6 +27,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang3.StringUtils; import org.apache.cxf.helpers.IOUtils; import org.apache.syncope.common.lib.to.Item; @@ -125,7 +126,19 @@ public class SAML2ClientCache { public SAML2Client add( final SAML2SP4UIIdP idp, final SAML2Configuration cfg, final String spEntityID, final String callbackUrl) { - cfg.setIdentityProviderMetadataResource((new ByteArrayResource(idp.getMetadata()))); + cfg.setIdentityProviderEntityId(idp.getEntityID()); + cfg.setIdentityProviderMetadataResource(new ByteArrayResource(idp.getMetadata())); + // remove when pac4j > 6.0.0-RC5 is available + cfg.setIdentityProviderMetadataResolver(new SAML2IdentityProviderMetadataResolver(cfg) { + + private final AtomicBoolean hasChanged = new AtomicBoolean(true); + + @Override + public boolean hasChanged() { + return hasChanged.getAndSet(false); + } + }); + cfg.setServiceProviderEntityId(spEntityID); getSPMetadataPath(spEntityID).ifPresent(cfg::setServiceProviderMetadataResourceFilepath); diff --git a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIContext.java b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIContext.java index 1722676508..834618401e 100644 --- a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIContext.java +++ b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIContext.java @@ -49,16 +49,14 @@ public class SAML2SP4UIContext implements WebContext { @Override public Optional<String> getRequestParameter(final String name) { - switch (name) { - case SAML2Constants.SAML_RESPONSE: - return Optional.ofNullable(saml2Response.getSamlResponse()); - - case SAML2Constants.RELAY_STATE: - return Optional.ofNullable(saml2Response.getRelayState()); - - default: - return Optional.empty(); - } + return switch (name) { + case SAML2Constants.SAML_RESPONSE -> + Optional.ofNullable(saml2Response.getSamlResponse()); + case SAML2Constants.RELAY_STATE -> + Optional.ofNullable(saml2Response.getRelayState()); + default -> + Optional.empty(); + }; } @Override diff --git a/fit/wa-reference/src/main/resources/log4j2.xml b/fit/wa-reference/src/main/resources/log4j2.xml index 9254c8ecab..744d540e4c 100644 --- a/fit/wa-reference/src/main/resources/log4j2.xml +++ b/fit/wa-reference/src/main/resources/log4j2.xml @@ -39,6 +39,9 @@ under the License. <asyncLogger name="org.apereo.cas" additivity="false" level="INFO"> <appender-ref ref="main"/> </asyncLogger> + <asyncLogger name="org.apereo.cas.support.saml.idp.metadata.generator" additivity="false" level="TRACE"> + <appender-ref ref="main"/> + </asyncLogger> <asyncLogger name="org.apereo.services.persondir" additivity="false" level="INFO"> <appender-ref ref="main"/> diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java index b96be4d323..d99e689221 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java @@ -180,8 +180,7 @@ public class SAML2SRAITCase extends AbstractSRAITCase { post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); try (CloseableHttpResponse response = httpclient.execute(post, context)) { assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode()); - location = response.getFirstHeader(HttpHeaders.LOCATION).getValue(). - replace("http://", "https://").replace(":8080", ":9443"); + location = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); } } diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/SAML2SP4UIITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/SAML2SP4UIITCase.java index fd37e60a1c..b7a38a7ad1 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/SAML2SP4UIITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/ui/SAML2SP4UIITCase.java @@ -257,8 +257,7 @@ public class SAML2SP4UIITCase extends AbstractUIITCase { post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); try (CloseableHttpResponse response = httpclient.execute(post, context)) { assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode()); - location = response.getFirstHeader(HttpHeaders.LOCATION).getValue(). - replace("http://", "https://").replace(":8080", ":9443"); + location = response.getFirstHeader(HttpHeaders.LOCATION).getValue(); } } diff --git a/pom.xml b/pom.xml index 94901713ee..d853988d73 100644 --- a/pom.xml +++ b/pom.xml @@ -437,7 +437,7 @@ under the License. <modernizer-maven.version>2.5.0</modernizer-maven.version> - <pac4j.version>6.0.0-RC5-SNAPSHOT</pac4j.version> + <pac4j.version>6.0.0-RC5</pac4j.version> <cas.version>7.0.0-SNAPSHOT</cas.version> <cas-client.version>4.0.1</cas-client.version> @@ -1687,7 +1687,7 @@ under the License. <dependency> <groupId>com.puppycrawl.tools</groupId> <artifactId>checkstyle</artifactId> - <version>10.6.0</version> + <version>10.7.0</version> </dependency> </dependencies> <configuration> @@ -1749,7 +1749,7 @@ under the License. <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> - <version>3.1.0</version> + <version>3.2.1</version> <executions> <execution> <id>default-cli</id> diff --git a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java index 8f69eccfe4..878ed14660 100644 --- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java +++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java @@ -303,21 +303,19 @@ public class SecurityConfig { anyExchange().authenticated(); switch (props.getAmType()) { - case OIDC: - case OAUTH2: + case OIDC, OAUTH2 -> { OAuth2SecurityConfigUtils.forLogin(http, props.getAmType(), ctx); OAuth2SecurityConfigUtils.forLogout(builder, props.getAmType(), cacheManager, logoutRouteMatcher, ctx); http.oauth2ResourceServer().jwt().jwtDecoder(ctx.getBean(ReactiveJwtDecoder.class)); - break; + } - case SAML2: + case SAML2 -> saml2Client.ifAvailable(client -> { SAML2SecurityConfigUtils.forLogin(http, client, publicRouteMatcher); SAML2SecurityConfigUtils.forLogout(builder, client, cacheManager, logoutRouteMatcher, ctx); }); - break; - case CAS: + case CAS -> { CASSecurityConfigUtils.forLogin( http, props.getCas().getProtocol(), @@ -329,9 +327,10 @@ public class SecurityConfig { props.getCas().getServerPrefix(), logoutRouteMatcher, ctx); - break; + } - default: + default -> { + } } return builder.and().csrf().requireCsrfProtectionMatcher(csrfRouteMatcher).and().build(); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java index c56c23f701..63cbc6a68a 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java @@ -19,6 +19,7 @@ package org.apache.syncope.sra.security.pac4j; import java.net.URI; +import java.util.Optional; import org.pac4j.core.exception.http.RedirectionAction; import org.pac4j.core.exception.http.WithContentAction; import org.pac4j.core.exception.http.WithLocationAction; @@ -32,27 +33,24 @@ public final class RedirectionActionUtils { final RedirectionAction action, final ServerWebExchangeContext swec) { - if (action instanceof WithLocationAction) { - WithLocationAction withLocationAction = (WithLocationAction) action; + if (action instanceof WithLocationAction withLocationAction) { swec.getNative().getResponse().setStatusCode(HttpStatus.FOUND); swec.getNative().getResponse().getHeaders().setLocation(URI.create(withLocationAction.getLocation())); return swec.getNative().getResponse().setComplete(); - } else if (action instanceof WithContentAction) { - WithContentAction withContentAction = (WithContentAction) action; - String content = withContentAction.getContent(); + } - if (content == null) { - throw new IllegalArgumentException("No content set for POST AuthnRequest"); - } + if (action instanceof WithContentAction withContentAction) { + String content = Optional.ofNullable(withContentAction.getContent()). + orElseThrow(() -> new IllegalArgumentException("No content set for POST AuthnRequest")); return Mono.defer(() -> { swec.getNative().getResponse().getHeaders().setContentType(MediaType.TEXT_HTML); return swec.getNative().getResponse(). writeWith(Mono.just(swec.getNative().getResponse().bufferFactory().wrap(content.getBytes()))); }); - } else { - throw new IllegalArgumentException("Unsupported Action: " + action.getClass().getName()); } + + throw new IllegalArgumentException("Unsupported Action: " + action.getClass().getName()); } private RedirectionActionUtils() { diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java index 74e712eb26..c99801dac7 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AuthenticationToken.java @@ -18,9 +18,11 @@ */ package org.apache.syncope.sra.security.saml2; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; -import org.pac4j.saml.credentials.SAML2Credentials; +import org.pac4j.saml.credentials.SAML2AuthenticationCredentials; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -28,11 +30,12 @@ public class SAML2AuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 8322987617416135717L; - private final SAML2Credentials credentials; + private final SAML2AuthenticationCredentials credentials; - public SAML2AuthenticationToken(final SAML2Credentials credentials) { - super(credentials.getUserProfile().getRoles().stream(). - map(SimpleGrantedAuthority::new).collect(Collectors.toSet())); + public SAML2AuthenticationToken(final SAML2AuthenticationCredentials credentials) { + super(Optional.ofNullable(credentials.getUserProfile()). + map(p -> p.getRoles().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet())). + orElse(Set.of())); this.credentials = credentials; this.setAuthenticated(true); } @@ -43,7 +46,7 @@ public class SAML2AuthenticationToken extends AbstractAuthenticationToken { } @Override - public SAML2Credentials getPrincipal() { + public SAML2AuthenticationCredentials getPrincipal() { return credentials; } } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java index 8d543159af..0f55618a88 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java @@ -18,21 +18,13 @@ */ package org.apache.syncope.sra.security.saml2; -import java.util.Optional; -import org.apache.syncope.sra.SessionConfig; import org.apache.syncope.sra.security.pac4j.NoOpSessionStore; -import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.core.context.CallContext; -import org.pac4j.core.exception.http.OkAction; -import org.pac4j.core.exception.http.RedirectionAction; -import org.pac4j.core.profile.factory.ProfileManagerFactory; -import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.core.credentials.Credentials; import org.pac4j.saml.client.SAML2Client; -import org.pac4j.saml.context.SAML2MessageContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cache.CacheManager; import org.springframework.http.HttpMethod; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; @@ -51,46 +43,28 @@ public class SAML2LogoutResponseWebFilter implements WebFilter { public static final ServerWebExchangeMatcher MATCHER = ServerWebExchangeMatchers.pathMatchers("/logout/saml2/sso"); - private static class ServerWebExchangeLogoutContext extends ServerWebExchangeContext { - - ServerWebExchangeLogoutContext(final ServerWebExchange exchange) { - super(exchange); - } - - @Override - public Optional<String> getRequestParameter(final String name) { - return Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER.equals(name) - ? Optional.of("true") - : super.getRequestParameter(name); - } - } - private final SAML2Client saml2Client; private final ServerLogoutSuccessHandler logoutSuccessHandler; - private final CacheManager cacheManager; - public SAML2LogoutResponseWebFilter( final SAML2Client saml2Client, - final SAML2ServerLogoutSuccessHandler logoutSuccessHandler, - final CacheManager cacheManager) { + final SAML2ServerLogoutSuccessHandler logoutSuccessHandler) { this.saml2Client = saml2Client; this.logoutSuccessHandler = logoutSuccessHandler; - this.cacheManager = cacheManager; } private Mono<Void> handleLogoutResponse( final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) { try { - SAML2MessageContext ctx = saml2Client.getContextProvider().buildContext( - new CallContext(swec, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT), - this.saml2Client); - saml2Client.getLogoutProfileHandler().receive(ctx); - } catch (OkAction e) { - LOG.debug("LogoutResponse was actually validated but no postLogoutURL was set", e); + CallContext ctx = new CallContext(swec, NoOpSessionStore.INSTANCE); + Credentials creds = saml2Client.getCredentialsExtractor(). + extract(ctx). + orElseThrow(() -> new IllegalStateException("Could not extract credentials")); + + saml2Client.getLogoutProcessor().processLogout(ctx, creds); } catch (Exception e) { LOG.error("Could not validate LogoutResponse", e); } @@ -98,47 +72,20 @@ public class SAML2LogoutResponseWebFilter implements WebFilter { return logoutSuccessHandler.onLogoutSuccess(new WebFilterExchange(exchange, chain), null); } - private Mono<Void> handleLogoutRequest( - final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) { - - return exchange.getSession(). - switchIfEmpty(chain.filter(exchange).then(Mono.empty())). - flatMap(session -> { - cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); - - return session.invalidate().then(Mono.defer(() -> { - try { - saml2Client.getCredentialsExtractor().extract(new CallContext( - swec, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)); - } catch (RedirectionAction action) { - return RedirectionActionUtils.handle(action, swec); - } - - return chain.filter(exchange).then(Mono.empty()); - })); - }); - } - private Mono<Void> handleGET(final ServerWebExchange exchange, final WebFilterChain chain) { if (exchange.getRequest().getQueryParams().getFirst("SAMLResponse") != null) { return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange)); - } else if (exchange.getRequest().getQueryParams().getFirst("SAMLRequest") != null) { - return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange)); } return chain.filter(exchange).then(Mono.empty()); } private Mono<Void> handlePOST(final ServerWebExchange exchange, final WebFilterChain chain) { - return exchange.getFormData().flatMap(form -> { - if (form.containsKey("SAMLResponse")) { - return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange).setForm(form)); - } else if (form.containsKey("SAMLRequest")) { - return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange).setForm(form)); - } - - return chain.filter(exchange).then(Mono.empty()); - }); + return exchange.getFormData(). + filter(form -> form.containsKey("SAMLResponse")). + flatMap(form -> handleLogoutResponse( + exchange, chain, new ServerWebExchangeContext(exchange).setForm(form))). + or(chain.filter(exchange).then(Mono.empty())); } @Override diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestServerLogoutHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestServerLogoutHandler.java index 6854acb90c..57f5fcee80 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestServerLogoutHandler.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestServerLogoutHandler.java @@ -23,9 +23,8 @@ import org.apache.syncope.sra.security.pac4j.NoOpSessionStore; import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.core.context.CallContext; -import org.pac4j.core.profile.factory.ProfileManagerFactory; import org.pac4j.saml.client.SAML2Client; -import org.pac4j.saml.credentials.SAML2Credentials; +import org.pac4j.saml.credentials.SAML2AuthenticationCredentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; @@ -51,7 +50,8 @@ public class SAML2RequestServerLogoutHandler implements ServerLogoutHandler { public Mono<Void> logout(final WebFilterExchange exchange, final Authentication authentication) { return exchange.getExchange().getSession(). flatMap(session -> { - SAML2Credentials credentials = (SAML2Credentials) authentication.getPrincipal(); + SAML2AuthenticationCredentials credentials = + (SAML2AuthenticationCredentials) authentication.getPrincipal(); LOG.debug("Creating SAML2 SP Logout Request for IDP[{}] and Profile[{}]", saml2Client.getIdentityProviderResolvedEntityId(), credentials.getUserProfile()); @@ -60,9 +60,7 @@ public class SAML2RequestServerLogoutHandler implements ServerLogoutHandler { cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); return session.invalidate().then(saml2Client.getLogoutAction( - new CallContext(swec, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT), - credentials.getUserProfile(), - null). + new CallContext(swec, NoOpSessionStore.INSTANCE), credentials.getUserProfile(), null). map(action -> RedirectionActionUtils.handle(action, swec)). orElseThrow(() -> new IllegalStateException("No action generated"))); }).onErrorResume(Mono::error); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java index a7be69c990..21b8923e8d 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java @@ -84,7 +84,7 @@ public final class SAML2SecurityConfigUtils { SAML2ServerLogoutSuccessHandler.class); SAML2LogoutResponseWebFilter logoutResponseWebFilter = - new SAML2LogoutResponseWebFilter(saml2Client, logoutSuccessHandler, cacheManager); + new SAML2LogoutResponseWebFilter(saml2Client, logoutSuccessHandler); builder.and().addFilterAt(logoutResponseWebFilter, SecurityWebFiltersOrder.LOGOUT); } catch (ClassNotFoundException e) { LOG.error("While creating instance of {}", SAML2ServerLogoutSuccessHandler.class.getName(), e); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java index a895ff299c..a504bc9711 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java @@ -18,11 +18,13 @@ */ package org.apache.syncope.sra.security.saml2; +import java.net.URI; import org.apache.syncope.sra.security.pac4j.NoOpSessionStore; import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; +import org.apache.syncope.sra.session.SessionUtils; import org.pac4j.core.context.CallContext; -import org.pac4j.core.profile.factory.ProfileManagerFactory; +import org.pac4j.core.util.generator.ValueGenerator; import org.pac4j.saml.client.SAML2Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,14 +56,23 @@ public class SAML2WebSsoAuthenticationRequestWebFilter implements WebFilter { return MATCHER.matches(exchange). filter(MatchResult::isMatch). switchIfEmpty(chain.filter(exchange).then(Mono.empty())). - flatMap(matchResult -> { + flatMap(r -> exchange.getSession()). + flatMap(session -> { LOG.debug("Creating SAML2 SP Authentication Request for IDP[{}]", saml2Client.getIdentityProviderResolvedEntityId()); + saml2Client.setStateGenerator(new ValueGenerator() { + + @Override + public String generateValue(final CallContext ctx) { + return session.<URI>getRequiredAttribute(SessionUtils.INITIAL_REQUEST_URI).toASCIIString(); + } + }); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange); return saml2Client.getRedirectionAction( - new CallContext(swec, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)). + new CallContext(swec, NoOpSessionStore.INSTANCE)). map(action -> RedirectionActionUtils.handle(action, swec)). orElseThrow(() -> new IllegalStateException("No action generated")); }).onErrorResume(Mono::error); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java index a35e436f11..90f8346cce 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java @@ -22,11 +22,10 @@ import java.net.URI; import org.apache.syncope.sra.security.pac4j.NoOpSessionStore; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy; -import org.apache.syncope.sra.session.SessionUtils; import org.pac4j.core.context.CallContext; -import org.pac4j.core.profile.factory.ProfileManagerFactory; +import org.pac4j.core.credentials.Credentials; import org.pac4j.saml.client.SAML2Client; -import org.pac4j.saml.credentials.SAML2Credentials; +import org.pac4j.saml.credentials.SAML2AuthenticationCredentials; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.ServerRedirectStrategy; @@ -71,7 +70,7 @@ public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter private ServerWebExchangeMatcher matchSamlResponse() { return exchange -> exchange.getFormData(). - filter(form -> form.containsKey("SAMLResponse")). + filter(form -> form.containsKey("SAMLResponse") && form.containsKey("RelayState")). flatMap(form -> ServerWebExchangeMatcher.MatchResult.match()). switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); } @@ -79,18 +78,19 @@ public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter private ServerAuthenticationConverter convertSamlResponse() { return exchange -> exchange.getFormData(). flatMap(form -> MATCHER.matches(exchange). - flatMap(matchResult -> { + flatMap(r -> { ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange).setForm(form); + CallContext ctx = new CallContext(swec, NoOpSessionStore.INSTANCE); - SAML2Credentials credentials = (SAML2Credentials) saml2Client.getCredentialsExtractor(). - extract(new CallContext( - swec, NoOpSessionStore.INSTANCE, ProfileManagerFactory.DEFAULT)). - orElseThrow(() -> new IllegalStateException("No AuthnResponse found")); + Credentials creds = saml2Client.getCredentialsExtractor(). + extract(ctx). + orElseThrow(() -> new IllegalStateException("Could not extract credentials")); - saml2Client.getAuthenticator().validate( - new CallContext(swec, NoOpSessionStore.INSTANCE), credentials); + SAML2AuthenticationCredentials authCreds = saml2Client.validateCredentials(ctx, creds). + map(SAML2AuthenticationCredentials.class::cast). + orElseThrow(() -> new IllegalArgumentException("Invalid SAML credentials provided")); - return Mono.just(new SAML2AuthenticationToken(credentials)); + return Mono.just(new SAML2AuthenticationToken(authCreds)); })); } @@ -103,10 +103,10 @@ public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter public Mono<Void> onAuthenticationSuccess( final WebFilterExchange webFilterExchange, final Authentication authentication) { - return webFilterExchange.getExchange().getSession(). - flatMap(session -> this.redirectStrategy.sendRedirect( + return webFilterExchange.getExchange().getFormData(). + flatMap(form -> this.redirectStrategy.sendRedirect( webFilterExchange.getExchange(), - session.<URI>getRequiredAttribute(SessionUtils.INITIAL_REQUEST_URI))); + URI.create(form.get("RelayState").get(0)))); } }; } diff --git a/sra/src/main/resources/sra.properties b/sra/src/main/resources/sra.properties index 1e42d3efbc..ea56675260 100644 --- a/sra/src/main/resources/sra.properties +++ b/sra/src/main/resources/sra.properties @@ -23,6 +23,7 @@ server.port=8080 management.endpoint.gateway.enabled=true management.endpoints.web.exposure.include=info,health,loggers,metrics,gateway,sraSessions management.endpoint.health.show-details=ALWAYS +management.endpoint.env.show-values=WHEN_AUTHORIZED spring.cloud.discovery.client.health-indicator.enabled=false service.discovery.address=http://localhost:8080/ diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java index 9a91ba0260..feb0743019 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/WAContext.java @@ -18,6 +18,7 @@ */ package org.apache.syncope.wa.starter.config; +import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.warrenstrange.googleauth.IGoogleAuthenticator; @@ -60,8 +61,9 @@ import org.apache.syncope.wa.starter.mapping.RegisteredServiceMapper; import org.apache.syncope.wa.starter.mapping.SAML2SPClientAppTOMapper; import org.apache.syncope.wa.starter.oidc.WAOIDCJWKSGeneratorService; import org.apache.syncope.wa.starter.pac4j.saml.WASAML2ClientCustomizer; -import org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataGenerator; -import org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataLocator; +import org.apache.syncope.wa.starter.saml.idp.WASamlIdPCasEventListener; +import org.apache.syncope.wa.starter.saml.idp.metadata.WASamlIdPMetadataGenerator; +import org.apache.syncope.wa.starter.saml.idp.metadata.WASamlIdPMetadataLocator; import org.apache.syncope.wa.starter.services.WAServiceRegistry; import org.apache.syncope.wa.starter.surrogate.WASurrogateAuthenticationService; import org.apache.syncope.wa.starter.u2f.WAU2FDeviceRepository; @@ -82,9 +84,11 @@ import org.apereo.cas.support.events.CasEventRepository; import org.apereo.cas.support.events.CasEventRepositoryFilter; import org.apereo.cas.support.pac4j.authentication.clients.DelegatedClientFactoryCustomizer; import org.apereo.cas.support.pac4j.authentication.handler.support.DelegatedClientAuthenticationHandler; +import org.apereo.cas.support.saml.idp.SamlIdPCasEventListener; import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator; import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext; import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator; +import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument; import org.apereo.cas.util.DateTimeUtils; import org.apereo.cas.util.LdapUtils; import org.apereo.cas.util.crypto.CipherExecutor; @@ -98,6 +102,7 @@ import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.ScopedProxyMode; @Configuration(proxyBeanMethods = false) @@ -221,36 +226,49 @@ public class WAContext { @Bean public ServiceRegistryExecutionPlanConfigurer syncopeServiceRegistryConfigurer( final ConfigurableApplicationContext ctx, - final WARestClient restClient, + final WARestClient waRestClient, final RegisteredServiceMapper registeredServiceMapper, @Qualifier("serviceRegistryListeners") final ObjectProvider<List<ServiceRegistryListener>> serviceRegistryListeners) { WAServiceRegistry registry = new WAServiceRegistry( - restClient, registeredServiceMapper, ctx, + waRestClient, registeredServiceMapper, ctx, Optional.ofNullable(serviceRegistryListeners.getIfAvailable()).orElseGet(ArrayList::new)); return plan -> plan.registerServiceRegistry(registry); } + @Bean + @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) + @Lazy(false) + public SamlIdPCasEventListener samlIdPCasEventListener() { + return new WASamlIdPCasEventListener(); + } + @Bean public SamlIdPMetadataGenerator samlIdPMetadataGenerator( - final WARestClient restClient, + final WARestClient waRestClient, final SamlIdPMetadataGeneratorConfigurationContext context) { - return new RestfulSamlIdPMetadataGenerator(context, restClient); + return new WASamlIdPMetadataGenerator(context, waRestClient); } @Bean - public SamlIdPMetadataLocator samlIdPMetadataLocator(final WARestClient restClient) { - return new RestfulSamlIdPMetadataLocator( - CipherExecutor.noOpOfStringToString(), - Caffeine.newBuilder().build(), - restClient); + public SamlIdPMetadataLocator samlIdPMetadataLocator( + @Qualifier("samlIdPMetadataGeneratorCipherExecutor") + final CipherExecutor<String, String> cipherExecutor, + @Qualifier("samlIdPMetadataCache") + final Cache<String, SamlIdPMetadataDocument> samlIdPMetadataCache, + final WARestClient waRestClient) { + + return new WASamlIdPMetadataLocator( + cipherExecutor, + samlIdPMetadataCache, + waRestClient); } @Bean - public AuditTrailExecutionPlanConfigurer auditConfigurer(final WARestClient restClient) { - return plan -> plan.registerAuditTrailManager(new WAAuditTrailManager(restClient)); + public AuditTrailExecutionPlanConfigurer auditConfigurer(final WARestClient waRestClient) { + return plan -> plan.registerAuditTrailManager(new WAAuditTrailManager(waRestClient)); } @ConditionalOnMissingBean(name = "syncopeWAEventRepositoryFilter") @@ -261,25 +279,25 @@ public class WAContext { @Bean public CasEventRepository casEventRepository( - final WARestClient restClient, + final WARestClient waRestClient, @Qualifier("syncopeWAEventRepositoryFilter") final CasEventRepositoryFilter syncopeWAEventRepositoryFilter) { - return new WAEventRepository(syncopeWAEventRepositoryFilter, restClient); + return new WAEventRepository(syncopeWAEventRepositoryFilter, waRestClient); } @Bean - public DelegatedClientFactoryCustomizer<Client> delegatedClientCustomizer(final WARestClient restClient) { - return new WASAML2ClientCustomizer(restClient); + public DelegatedClientFactoryCustomizer<Client> delegatedClientCustomizer(final WARestClient waRestClient) { + return new WASAML2ClientCustomizer(waRestClient); } @Bean public WAGoogleMfaAuthTokenRepository oneTimeTokenAuthenticatorTokenRepository( final CasConfigurationProperties casProperties, - final WARestClient restClient) { + final WARestClient waRestClient) { return new WAGoogleMfaAuthTokenRepository( - restClient, casProperties.getAuthn().getMfa().getGauth().getCore().getTimeStepSize()); + waRestClient, casProperties.getAuthn().getMfa().getGauth().getCore().getTimeStepSize()); } @ConditionalOnMissingBean(name = CUSTOM_GOOGLE_AUTHENTICATOR_ACCOUNT_REGISTRY) @@ -292,7 +310,7 @@ public class WAContext { @Qualifier("googleAuthenticatorScratchCodesCipherExecutor") final CipherExecutor<Number, Number> googleAuthenticatorScratchCodesCipherExecutor, final IGoogleAuthenticator googleAuthenticatorInstance, - final WARestClient restClient) { + final WARestClient waRestClient) { /* * Declaring the LDAP-based repository as a Spring bean that would be conditionally activated @@ -314,16 +332,16 @@ public class WAContext { connectionFactory, ldap); } - return new WAGoogleMfaAuthCredentialRepository(restClient, googleAuthenticatorInstance); + return new WAGoogleMfaAuthCredentialRepository(waRestClient, googleAuthenticatorInstance); } @Bean public OidcJsonWebKeystoreGeneratorService oidcJsonWebKeystoreGeneratorService( final CasConfigurationProperties casProperties, - final WARestClient restClient) { + final WARestClient waRestClient) { return new WAOIDCJWKSGeneratorService( - restClient, + waRestClient, casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeyId(), casProperties.getAuthn().getOidc().getJwks().getCore().getJwksType(), casProperties.getAuthn().getOidc().getJwks().getCore().getJwksKeySize()); @@ -332,15 +350,15 @@ public class WAContext { @Bean public WebAuthnCredentialRepository webAuthnCredentialRepository( final CasConfigurationProperties casProperties, - final WARestClient restClient) { + final WARestClient waRestClient) { - return new WAWebAuthnCredentialRepository(casProperties, restClient); + return new WAWebAuthnCredentialRepository(casProperties, waRestClient); } @Bean public U2FDeviceRepository u2fDeviceRepository( final CasConfigurationProperties casProperties, - final WARestClient restClient) { + final WARestClient waRestClient) { U2FCoreMultifactorAuthenticationProperties u2f = casProperties.getAuthn().getMfa().getU2f().getCore(); OffsetDateTime expirationDate = OffsetDateTime.now(). @@ -348,18 +366,18 @@ public class WAContext { LoadingCache<String, String> requestStorage = Caffeine.newBuilder(). expireAfterWrite(u2f.getExpireRegistrations(), u2f.getExpireRegistrationsTimeUnit()). build(key -> StringUtils.EMPTY); - return new WAU2FDeviceRepository(casProperties, requestStorage, restClient, expirationDate); + return new WAU2FDeviceRepository(casProperties, requestStorage, waRestClient, expirationDate); } @Bean - public SurrogateAuthenticationService surrogateAuthenticationService(final WARestClient restClient) { - return new WASurrogateAuthenticationService(restClient); + public SurrogateAuthenticationService surrogateAuthenticationService(final WARestClient waRestClient) { + return new WASurrogateAuthenticationService(waRestClient); } @ConditionalOnMissingBean @Bean - public SyncopeCoreHealthIndicator syncopeCoreHealthIndicator(final WARestClient restClient) { - return new SyncopeCoreHealthIndicator(restClient); + public SyncopeCoreHealthIndicator syncopeCoreHealthIndicator(final WARestClient waRestClient) { + return new SyncopeCoreHealthIndicator(waRestClient); } @ConditionalOnMissingBean diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/JWSAlgorithm.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPCasEventListener.java similarity index 61% rename from common/am/lib/src/main/java/org/apache/syncope/common/lib/types/JWSAlgorithm.java rename to wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPCasEventListener.java index 015a9d2ec7..da1bcaf72c 100644 --- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/JWSAlgorithm.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPCasEventListener.java @@ -16,22 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.syncope.common.lib.types; +package org.apache.syncope.wa.starter.saml.idp; -public enum JWSAlgorithm { - HS256, - HS384, - HS512, - RS256, - RS384, - RS512, - ES256, - ES384, - ES512, - PS256, - PS384, - PS512, - EdDSA, - ES256K; +import org.apereo.cas.support.saml.idp.SamlIdPCasEventListener; +import org.springframework.boot.context.event.ApplicationReadyEvent; +public class WASamlIdPCasEventListener implements SamlIdPCasEventListener { + + @Override + public void handleApplicationReadyEvent(final ApplicationReadyEvent event) { + // skip generating IdP metadata at this stage, as + // org.apereo.cas.support.saml.idp.DefaultSamlIdPCasEventListener is doing + } } diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataGenerator.java similarity index 82% rename from wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java rename to wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataGenerator.java index 2161f118ce..07a8227ee8 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataGenerator.java @@ -33,13 +33,13 @@ import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class RestfulSamlIdPMetadataGenerator extends BaseSamlIdPMetadataGenerator { +public class WASamlIdPMetadataGenerator extends BaseSamlIdPMetadataGenerator { - private static final Logger LOG = LoggerFactory.getLogger(RestfulSamlIdPMetadataGenerator.class); + private static final Logger LOG = LoggerFactory.getLogger(WASamlIdPMetadataGenerator.class); private final WARestClient waRestClient; - public RestfulSamlIdPMetadataGenerator( + public WASamlIdPMetadataGenerator( final SamlIdPMetadataGeneratorConfigurationContext samlIdPMetadataGeneratorConfigurationContext, final WARestClient waRestClient) { @@ -47,14 +47,34 @@ public class RestfulSamlIdPMetadataGenerator extends BaseSamlIdPMetadataGenerato this.waRestClient = waRestClient; } + @Override + public String getAppliesToFor(final Optional<SamlRegisteredService> registeredService) { + return registeredService. + map(SamlRegisteredService::getName). + orElse(SAML2IdPEntityService.DEFAULT_OWNER); + } + + private SyncopeClient getSyncopeClient() { + if (!waRestClient.isReady()) { + LOG.info("Syncope client is not yet ready"); + throw new IllegalStateException("Syncope core is not yet ready to access requests"); + } + return waRestClient.getSyncopeClient(); + } + + @Override + public SamlIdPMetadataDocument generate(final Optional<SamlRegisteredService> registeredService) throws Exception { + return super.generate(registeredService); + } + @Override protected SamlIdPMetadataDocument finalizeMetadataDocument( final SamlIdPMetadataDocument doc, final Optional<SamlRegisteredService> registeredService) throws Exception { - LOG.info("Generating new SAML2 IdP metadata document"); + doc.setAppliesTo(getAppliesToFor(registeredService)); - doc.setAppliesTo(SAML2IdPEntityService.DEFAULT_OWNER); + LOG.info("Setting new SAML2 IdP metadata document for {}", doc.getAppliesTo()); SAML2IdPEntityTO entityTO = new SAML2IdPEntityTO.Builder(). key(doc.getAppliesTo()). @@ -84,23 +104,15 @@ public class RestfulSamlIdPMetadataGenerator extends BaseSamlIdPMetadataGenerato @Override public Pair<String, String> buildSelfSignedEncryptionCert(final Optional<SamlRegisteredService> registeredService) - throws Exception { + throws Exception { return generateCertificateAndKey(); } @Override public Pair<String, String> buildSelfSignedSigningCert(final Optional<SamlRegisteredService> registeredService) - throws Exception { + throws Exception { return generateCertificateAndKey(); } - - private SyncopeClient getSyncopeClient() { - if (!waRestClient.isReady()) { - LOG.info("Syncope client is not yet ready"); - throw new IllegalStateException("Syncope core is not yet ready to access requests"); - } - return waRestClient.getSyncopeClient(); - } } diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataLocator.java similarity index 88% rename from wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java rename to wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataLocator.java index 71c8fa1d5e..fa490e195f 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/WASamlIdPMetadataLocator.java @@ -35,13 +35,13 @@ import org.apereo.cas.util.crypto.CipherExecutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocator { +public class WASamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocator { - private static final Logger LOG = LoggerFactory.getLogger(RestfulSamlIdPMetadataLocator.class); + private static final Logger LOG = LoggerFactory.getLogger(WASamlIdPMetadataLocator.class); private final WARestClient waRestClient; - public RestfulSamlIdPMetadataLocator( + public WASamlIdPMetadataLocator( final CipherExecutor<String, String> metadataCipherExecutor, final Cache<String, SamlIdPMetadataDocument> metadataCache, final WARestClient waRestClient) { @@ -50,6 +50,13 @@ public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocato this.waRestClient = waRestClient; } + @Override + public String getAppliesToFor(final Optional<SamlRegisteredService> registeredService) { + return registeredService. + map(SamlRegisteredService::getName). + orElse(SAML2IdPEntityService.DEFAULT_OWNER); + } + @Override public SamlIdPMetadataDocument fetchInternal(final Optional<SamlRegisteredService> registeredService) { try { @@ -94,7 +101,7 @@ public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocato if (LOG.isDebugEnabled()) { LOG.error("While fetching SAML2 IdP metadata", e); } else { - LOG.error("While fetching SAML2 IdP metadata: " + e.getMessage()); + LOG.error("While fetching SAML2 IdP metadata: {}", e.getMessage()); } } } @@ -103,16 +110,14 @@ public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocato } private SAML2IdPEntityTO fetchFromCore(final Optional<SamlRegisteredService> registeredService) { - SAML2IdPEntityTO result = null; + SAML2IdPEntityService idpEntityService = getSyncopeClient().getService(SAML2IdPEntityService.class); - String appliesToFor = registeredService.map(SamlRegisteredService::getName). - orElse(SAML2IdPEntityService.DEFAULT_OWNER); - SAML2IdPEntityService service = getSyncopeClient().getService(SAML2IdPEntityService.class); + SAML2IdPEntityTO result = null; try { - result = service.get(appliesToFor); + result = idpEntityService.get(getAppliesToFor(registeredService)); } catch (SyncopeClientException e) { if (e.getType() == ClientExceptionType.NotFound && registeredService.isPresent()) { - result = service.get(SAML2IdPEntityService.DEFAULT_OWNER); + result = idpEntityService.get(SAML2IdPEntityService.DEFAULT_OWNER); } else { throw e; } diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/services/WAServiceRegistry.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/services/WAServiceRegistry.java index 523be1164c..29665b7afe 100644 --- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/services/WAServiceRegistry.java +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/services/WAServiceRegistry.java @@ -20,6 +20,7 @@ package org.apache.syncope.wa.starter.services; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import org.apache.syncope.client.lib.SyncopeClient; import org.apache.syncope.common.lib.types.ClientAppType; @@ -142,14 +143,15 @@ public class WAServiceRegistry extends AbstractServiceRegistry { @Override public RegisteredService findServiceByExactServiceName(final String name) { - SyncopeClient syncopeClient = waRestClient.getSyncopeClient(); - if (syncopeClient == null) { - LOG.debug("Syncope client is not yet ready to fetch application definitions"); - return null; - } else { - LOG.info("Searching for application definition by name {}", name); - return registeredServiceMapper.toRegisteredService(waRestClient.getSyncopeClient(). - getService(WAClientAppService.class).read(name, null)); - } + return Optional.ofNullable(waRestClient.getSyncopeClient()). + map(syncopeClient -> { + LOG.info("Searching for application definition by name {}", name); + return registeredServiceMapper.toRegisteredService(waRestClient.getSyncopeClient(). + getService(WAClientAppService.class).read(name, null)); + }). + orElseGet(() -> { + LOG.debug("Syncope client is not yet ready to fetch application definitions"); + return null; + }); } } diff --git a/wa/starter/src/main/resources/wa.properties b/wa/starter/src/main/resources/wa.properties index 8d16065d89..7c8ccd378f 100644 --- a/wa/starter/src/main/resources/wa.properties +++ b/wa/starter/src/main/resources/wa.properties @@ -39,6 +39,7 @@ cas.monitor.endpoints.endpoint.defaults.access=AUTHENTICATED management.endpoints.enabled-by-default=true management.endpoints.web.exposure.include=info,health,env,loggers,ssoSessions,registeredServices,refresh,authenticationHandlers,authenticationPolicies,resolveAttributes management.endpoint.health.show-details=ALWAYS +management.endpoint.env.show-values=WHEN_AUTHORIZED spring.cloud.discovery.client.health-indicator.enabled=false # Cache service definitions for 5 minutes