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 bbab15e69c..524e27010f 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 @@ -131,7 +131,13 @@ public SAML2Client add( cfg.setServiceProviderEntityId(spEntityID); getSPMetadataPath(spEntityID).ifPresent(cfg::setServiceProviderMetadataResourceFilepath); - SAML2Client saml2Client = new SAML2Client(cfg); + SAML2Client saml2Client = new SAML2Client(cfg) { + + @Override + protected void initSignatureSigningParametersProvider() { + signatureSigningParametersProvider = new SAML2SP4UISignatureSigningParametersProvider(configuration); + } + }; saml2Client.setCallbackUrlResolver(new NoParameterCallbackUrlResolver()); saml2Client.setCallbackUrl(callbackUrl); saml2Client.init(); diff --git a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UISignatureSigningParametersProvider.java b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UISignatureSigningParametersProvider.java new file mode 100644 index 0000000000..72a4e78669 --- /dev/null +++ b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UISignatureSigningParametersProvider.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.logic.saml2; + +import java.util.List; +import java.util.Optional; +import org.opensaml.xmlsec.SignatureSigningConfiguration; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.pac4j.saml.config.SAML2Configuration; +import org.pac4j.saml.crypto.DefaultSignatureSigningParametersProvider; + +public class SAML2SP4UISignatureSigningParametersProvider extends DefaultSignatureSigningParametersProvider { + + protected final SAML2Configuration configuration; + + public SAML2SP4UISignatureSigningParametersProvider(final SAML2Configuration configuration) { + super(configuration); + this.configuration = configuration; + } + + @Override + protected SignatureSigningConfiguration getSignatureSigningConfiguration() { + BasicSignatureSigningConfiguration ssc = + DefaultSecurityConfigurationBootstrap.buildDefaultSignatureSigningConfiguration(); + + Optional.ofNullable(configuration.getBlackListedSignatureSigningAlgorithms()). + ifPresent(ssc::setExcludedAlgorithms); + + Optional.ofNullable(configuration.getSignatureAlgorithms()). + ifPresent(ssc::setSignatureAlgorithms); + + Optional.ofNullable(configuration.getSignatureCanonicalizationAlgorithm()). + ifPresent(ssc::setSignatureCanonicalizationAlgorithm); + + Optional.ofNullable(configuration.getSignatureReferenceDigestMethods()). + ifPresent(ssc::setSignatureReferenceDigestMethods); + + ssc.setSigningCredentials(List.of(configuration.getCredentialProvider().getCredential())); + return ssc; + } +} 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 6eb86ee2b1..26cacc3291 100644 --- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java +++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java @@ -37,6 +37,7 @@ import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint; import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils; import org.apache.syncope.sra.security.saml2.SAML2WebSsoAuthenticationWebFilter; +import org.apache.syncope.sra.security.saml2.SRASignatureSigningParametersProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver; import org.pac4j.saml.client.SAML2Client; @@ -293,7 +294,13 @@ public InputStream retrieve() throws Exception { cfg.setSessionLogoutHandler(new NoOpSessionLogoutHandler()); - SAML2Client saml2Client = new SAML2Client(cfg); + SAML2Client saml2Client = new SAML2Client(cfg) { + + @Override + protected void initSignatureSigningParametersProvider() { + signatureSigningParametersProvider = new SRASignatureSigningParametersProvider(configuration); + } + }; saml2Client.setName(SRAProperties.AMType.SAML2.name()); saml2Client.setCallbackUrl(props.getSaml2().getEntityId() + SAML2WebSsoAuthenticationWebFilter.FILTER_PROCESSES_URI); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SRASignatureSigningParametersProvider.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SRASignatureSigningParametersProvider.java new file mode 100644 index 0000000000..4b2ad75a11 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SRASignatureSigningParametersProvider.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.sra.security.saml2; + +import java.util.List; +import java.util.Optional; +import org.opensaml.xmlsec.SignatureSigningConfiguration; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.impl.BasicSignatureSigningConfiguration; +import org.pac4j.saml.config.SAML2Configuration; +import org.pac4j.saml.crypto.DefaultSignatureSigningParametersProvider; + +public class SRASignatureSigningParametersProvider extends DefaultSignatureSigningParametersProvider { + + protected final SAML2Configuration configuration; + + public SRASignatureSigningParametersProvider(final SAML2Configuration configuration) { + super(configuration); + this.configuration = configuration; + } + + @Override + protected SignatureSigningConfiguration getSignatureSigningConfiguration() { + BasicSignatureSigningConfiguration ssc = + DefaultSecurityConfigurationBootstrap.buildDefaultSignatureSigningConfiguration(); + + Optional.ofNullable(configuration.getBlackListedSignatureSigningAlgorithms()). + ifPresent(ssc::setExcludedAlgorithms); + + Optional.ofNullable(configuration.getSignatureAlgorithms()). + ifPresent(ssc::setSignatureAlgorithms); + + Optional.ofNullable(configuration.getSignatureCanonicalizationAlgorithm()). + ifPresent(ssc::setSignatureCanonicalizationAlgorithm); + + Optional.ofNullable(configuration.getSignatureReferenceDigestMethods()). + ifPresent(ssc::setSignatureReferenceDigestMethods); + + ssc.setSigningCredentials(List.of(configuration.getCredentialProvider().getCredential())); + return ssc; + } +} diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml index c93e899206..4c80b4c1b9 100644 --- a/wa/starter/pom.xml +++ b/wa/starter/pom.xml @@ -203,6 +203,10 @@ under the License. org.apereo.cas cas-server-support-saml-idp-core + + org.apereo.cas + cas-server-support-saml-idp-web + org.apereo.cas cas-server-support-oidc 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 8871cfb159..ca2823f745 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 @@ -61,6 +61,7 @@ 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.WASamlIdPCasEventListener; +import org.apache.syncope.wa.starter.saml.idp.WASamlIdPObjectSigner; 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; @@ -89,11 +90,13 @@ 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.support.saml.web.idp.profile.builders.enc.SamlIdPObjectSigner; import org.apereo.cas.util.DateTimeUtils; import org.apereo.cas.util.LdapUtils; import org.apereo.cas.util.crypto.CipherExecutor; import org.apereo.cas.webauthn.storage.WebAuthnCredentialRepository; import org.ldaptive.ConnectionFactory; +import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.pac4j.core.client.Client; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Qualifier; @@ -259,10 +262,19 @@ public SamlIdPMetadataLocator samlIdPMetadataLocator( final Cache samlIdPMetadataCache, final WARestClient waRestClient) { - return new WASamlIdPMetadataLocator( - cipherExecutor, - samlIdPMetadataCache, - waRestClient); + return new WASamlIdPMetadataLocator(cipherExecutor, samlIdPMetadataCache, waRestClient); + } + + @Bean + @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT) + public SamlIdPObjectSigner samlObjectSigner( + final CasConfigurationProperties casProperties, + @Qualifier("casSamlIdPMetadataResolver") + final MetadataResolver casSamlIdPMetadataResolver, + @Qualifier("samlIdPMetadataLocator") + final SamlIdPMetadataLocator samlIdPMetadataLocator) { + + return new WASamlIdPObjectSigner(casSamlIdPMetadataResolver, casProperties, samlIdPMetadataLocator); } @Bean diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPObjectSigner.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPObjectSigner.java new file mode 100644 index 0000000000..a56c962796 --- /dev/null +++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/WASamlIdPObjectSigner.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.wa.starter.saml.idp; + +import com.google.common.collect.Sets; +import java.util.ArrayList; +import java.util.Objects; +import java.util.regex.Pattern; +import net.shibboleth.shared.resolver.CriteriaSet; +import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.support.saml.SamlIdPUtils; +import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataCredentialResolver; +import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator; +import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPSamlRegisteredServiceCriterion; +import org.apereo.cas.support.saml.services.SamlRegisteredService; +import org.apereo.cas.support.saml.web.idp.profile.builders.enc.DefaultSamlIdPObjectSigner; +import org.apereo.cas.util.DigestUtils; +import org.apereo.cas.util.RegexUtils; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.saml.criterion.EntityRoleCriterion; +import org.opensaml.saml.metadata.criteria.entity.impl.EvaluableEntityRoleEntityDescriptorCriterion; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.security.credential.AbstractCredential; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.criteria.UsageCriterion; +import org.opensaml.xmlsec.SignatureSigningConfiguration; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.criterion.SignatureSigningConfigurationCriterion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WASamlIdPObjectSigner extends DefaultSamlIdPObjectSigner { + + protected static final Logger LOG = LoggerFactory.getLogger(WASamlIdPObjectSigner.class); + + protected static boolean doesCredentialFingerprintMatch( + final AbstractCredential credential, + final SamlRegisteredService samlRegisteredService) { + + var fingerprint = samlRegisteredService.getSigningCredentialFingerprint(); + if (StringUtils.isNotBlank(fingerprint)) { + var digest = DigestUtils.digest("SHA-1", Objects.requireNonNull(credential.getPublicKey()).getEncoded()); + var pattern = RegexUtils.createPattern(fingerprint, Pattern.CASE_INSENSITIVE); + LOG.debug("Matching credential fingerprint [{}] against filter [{}] for service [{}]", + digest, fingerprint, samlRegisteredService.getName()); + return pattern.matcher(digest).find(); + } + return true; + } + + protected final MetadataResolver samlIdPMetadataResolver; + + protected final CasConfigurationProperties casProperties; + + protected final SamlIdPMetadataLocator samlIdPMetadataLocator; + + public WASamlIdPObjectSigner( + final MetadataResolver samlIdPMetadataResolver, + final CasConfigurationProperties casProperties, + final SamlIdPMetadataLocator samlIdPMetadataLocator) { + + super(samlIdPMetadataResolver, casProperties, samlIdPMetadataLocator); + this.samlIdPMetadataResolver = samlIdPMetadataResolver; + this.casProperties = casProperties; + this.samlIdPMetadataLocator = samlIdPMetadataLocator; + } + + @Override + protected SignatureSigningConfiguration getSignatureSigningConfiguration(final SamlRegisteredService service) + throws Exception { + + var config = configureSignatureSigningSecurityConfiguration(service); + + var samlIdp = casProperties.getAuthn().getSamlIdp(); + var privateKey = getSigningPrivateKey(service); + + var mdCredentialResolver = new SamlIdPMetadataCredentialResolver(); + var roleDescriptorResolver = SamlIdPUtils.getRoleDescriptorResolver( + samlIdPMetadataResolver, + samlIdp.getMetadata().getCore().isRequireValidMetadata()); + mdCredentialResolver.setRoleDescriptorResolver(roleDescriptorResolver); + mdCredentialResolver.setKeyInfoCredentialResolver( + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + mdCredentialResolver.initialize(); + + var criteriaSet = new CriteriaSet(); + criteriaSet.add(new SignatureSigningConfigurationCriterion(config)); + criteriaSet.add(new UsageCriterion(UsageType.SIGNING)); + + var entityIdCriteriaSet = new CriteriaSet( + new EvaluableEntityRoleEntityDescriptorCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME), + new SamlIdPSamlRegisteredServiceCriterion(service)); + LOG.trace("Resolving entity id from SAML2 IdP metadata for signature signing configuration is [{}]", service. + getName()); + var entityId = Objects.requireNonNull(samlIdPMetadataResolver.resolveSingle(entityIdCriteriaSet)).getEntityID(); + LOG.trace("Resolved entity id from SAML2 IdP metadata is [{}]", entityId); + criteriaSet.add(new EntityIdCriterion(entityId)); + criteriaSet.add(new EntityRoleCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)); + criteriaSet.add(new SamlIdPSamlRegisteredServiceCriterion(service)); + + LOG.trace("Resolved signing credentials based on criteria [{}]", criteriaSet); + var credentials = Sets.newLinkedHashSet(mdCredentialResolver.resolve(criteriaSet)); + LOG.trace("Resolved [{}] signing credentials", credentials.size()); + + var finalCredentials = new ArrayList(); + credentials.stream() + .map(creds -> getResolvedSigningCredential(creds, privateKey, service)) + .filter(Objects::nonNull) + .filter(creds -> doesCredentialFingerprintMatch(creds, service)) + .forEach(finalCredentials::add); + + if (finalCredentials.isEmpty()) { + LOG.error("Unable to locate any signing credentials for service [{}]", service.getName()); + throw new IllegalArgumentException("Unable to locate signing credentials"); + } + + config.setSigningCredentials(finalCredentials); + LOG.trace("Signature signing credentials configured with [{}] credentials", finalCredentials.size()); + return config; + } +}