You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by mm...@apache.org on 2022/06/03 14:20:52 UTC
[syncope] branch master updated: SYNCOPE-1681: Support LDAP backends for Google Authenticator MFA (#349)
This is an automated email from the ASF dual-hosted git repository.
mmoayyed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/master by this push:
new 9ecd2ed254 SYNCOPE-1681: Support LDAP backends for Google Authenticator MFA (#349)
9ecd2ed254 is described below
commit 9ecd2ed2548c538c45409376e83295103be844c9
Author: Misagh Moayyed <mm...@gmail.com>
AuthorDate: Fri Jun 3 18:20:47 2022 +0400
SYNCOPE-1681: Support LDAP backends for Google Authenticator MFA (#349)
* switch cas to use 6.5 rc2
* resume with boot 2.6 upgrade
* update spring cloud gateway
* upgrade to boot 2.6
* Fix test cases; make sure exceptions are caught in SAML2 metadata generation process
* assign a name to the syncope authn handler matching master-content and auth-module
* upgrade to spring boot 2.6; fixes build issues
* Support LDAP backend for Google Authenticator
* remove unused imports
* remove unused imports
* Update CAS version
---
.../common/lib/auth/GoogleMfaAuthModuleConf.java | 93 ++++++++++++++++++++++
.../syncope/core/logic/wa/WAConfigLogic.java | 3 +-
.../rest/cxf/service/AnyObjectServiceTest.java | 1 -
.../org/apache/syncope/fit/core/UserITCase.java | 1 -
fit/wa-reference/pom.xml | 6 ++
pom.xml | 12 ++-
.../bootstrap/SyncopeWAPropertySourceLocator.java | 26 ++++--
wa/starter/pom.xml | 8 ++
.../syncope/wa/starter/SyncopeWAApplication.java | 14 ++++
.../wa/starter/config/SyncopeWAConfiguration.java | 24 ++++++
wa/starter/src/main/resources/wa.properties | 2 +-
11 files changed, 177 insertions(+), 13 deletions(-)
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/GoogleMfaAuthModuleConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/GoogleMfaAuthModuleConf.java
index ef5f05b43f..f104575e35 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/GoogleMfaAuthModuleConf.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/GoogleMfaAuthModuleConf.java
@@ -52,6 +52,99 @@ public class GoogleMfaAuthModuleConf implements AuthModuleConf {
*/
private int windowSize = 3;
+ /**
+ * Name of LDAP attribute that holds GAuth account/credential as JSON.
+ */
+ private String ldapAccountAttributeName = "casGAuthRecord";
+
+ /**
+ * Base DN to use. There may be scenarios where different parts of a single LDAP tree
+ * could be considered as base-dns. Each entry can be specified
+ * and joined together using a special delimiter character.
+ */
+ private String ldapBaseDn;
+
+ /**
+ * The bind credential to use when connecting to LDAP.
+ */
+ private String ldapBindCredential;
+
+ /**
+ * The bind DN to use when connecting to LDAP.
+ */
+ private String ldapBindDn;
+
+ /**
+ * The LDAP url to the server. More than one may be specified, separated by space and/or comma.
+ */
+ private String ldapUrl;
+
+ /**
+ * User filter to use for searching. Syntax is i.e. cn={user} or cn={0}.
+ */
+ private String ldapSearchFilter;
+
+ /**
+ * Whether subtree searching is allowed.
+ */
+ private boolean ldapSubtreeSearch = true;
+
+ public String getLdapAccountAttributeName() {
+ return ldapAccountAttributeName;
+ }
+
+ public void setLdapAccountAttributeName(final String ldapAccountAttributeName) {
+ this.ldapAccountAttributeName = ldapAccountAttributeName;
+ }
+
+ public String getLdapBaseDn() {
+ return ldapBaseDn;
+ }
+
+ public void setLdapBaseDn(final String ldapBaseDn) {
+ this.ldapBaseDn = ldapBaseDn;
+ }
+
+ public String getLdapBindCredential() {
+ return ldapBindCredential;
+ }
+
+ public void setLdapBindCredential(final String ldapBindCredential) {
+ this.ldapBindCredential = ldapBindCredential;
+ }
+
+ public String getLdapBindDn() {
+ return ldapBindDn;
+ }
+
+ public void setLdapBindDn(final String ldapBindDn) {
+ this.ldapBindDn = ldapBindDn;
+ }
+
+ public String getLdapUrl() {
+ return ldapUrl;
+ }
+
+ public void setLdapUrl(final String ldapUrl) {
+ this.ldapUrl = ldapUrl;
+ }
+
+ public String getLdapSearchFilter() {
+ return ldapSearchFilter;
+ }
+
+ public void setLdapSearchFilter(final String ldapSearchFilter) {
+ this.ldapSearchFilter = ldapSearchFilter;
+ }
+
+ public boolean isLdapSubtreeSearch() {
+ return ldapSubtreeSearch;
+ }
+
+ public void setLdapSubtreeSearch(final boolean ldapSubtreeSearch) {
+ this.ldapSubtreeSearch = ldapSubtreeSearch;
+ }
+
public String getIssuer() {
return issuer;
}
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAConfigLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAConfigLogic.java
index cb103f3096..ab1b522c23 100644
--- a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAConfigLogic.java
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAConfigLogic.java
@@ -97,13 +97,14 @@ public class WAConfigLogic extends AbstractTransactionalLogic<EntityTO> {
public void pushToWA() {
try {
NetworkService wa = serviceOps.get(NetworkService.Type.WA);
- HttpClient.newBuilder().build().send(
+ HttpResponse response = HttpClient.newBuilder().build().send(
HttpRequest.newBuilder(URI.create(
StringUtils.appendIfMissing(wa.getAddress(), "/") + "actuator/refresh")).
header(HttpHeaders.AUTHORIZATION, DefaultBasicAuthSupplier.getBasicAuthHeader(
securityProperties.getAnonymousUser(), securityProperties.getAnonymousKey())).
POST(HttpRequest.BodyPublishers.noBody()).build(),
HttpResponse.BodyHandlers.discarding());
+ LOG.info("Pushed changes to WA with status: {}", response.statusCode());
} catch (KeymasterException e) {
throw new NotFoundException("Could not find any WA instance", e);
} catch (IOException | InterruptedException e) {
diff --git a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
index 5078c55fa8..4ee8d0e0b4 100644
--- a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
+++ b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
@@ -78,7 +78,6 @@ import org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper;
import org.apache.syncope.common.lib.jackson.SyncopeJsonMapper;
import org.apache.syncope.common.lib.jackson.SyncopeXmlMapper;
import org.apache.syncope.common.lib.jackson.SyncopeYAMLMapper;
-import org.apache.syncope.core.persistence.api.entity.Realm;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
index 3d85e73cfd..f2ddbe7bc1 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
@@ -31,7 +31,6 @@ import java.io.IOException;
import java.security.AccessControlException;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
diff --git a/fit/wa-reference/pom.xml b/fit/wa-reference/pom.xml
index a1e4ef9a93..8d916f908f 100644
--- a/fit/wa-reference/pom.xml
+++ b/fit/wa-reference/pom.xml
@@ -70,6 +70,12 @@ under the License.
<artifactId>syncope-sra</artifactId>
<version>${project.version}</version>
<scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <artifactId>spring-cloud-starter</artifactId>
+ <groupId>org.springframework.cloud</groupId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
<groupId>org.apache.syncope.fit</groupId>
diff --git a/pom.xml b/pom.xml
index b3369c40ff..55a6d10a0a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -448,7 +448,7 @@ under the License.
<pac4j.version>5.3.1</pac4j.version>
- <cas.version>6.5.4</cas.version>
+ <cas.version>6.5.5</cas.version>
<cas-client.version>3.6.4</cas-client.version>
<h2.version>2.1.212</h2.version>
@@ -1550,6 +1550,16 @@ under the License.
<artifactId>cas-server-support-gauth</artifactId>
<version>${cas.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-ldap-core</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-gauth-ldap</artifactId>
+ <version>${cas.version}</version>
+ </dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-duo</artifactId>
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
index 48c6644b7c..c7d514c799 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
@@ -53,6 +53,7 @@ import org.apereo.cas.configuration.model.support.ldap.LdapAuthenticationPropert
import org.apereo.cas.configuration.model.support.mfa.DuoSecurityMultifactorAuthenticationProperties;
import org.apereo.cas.configuration.model.support.mfa.MultifactorAuthenticationProperties;
import org.apereo.cas.configuration.model.support.mfa.gauth.GoogleAuthenticatorMultifactorProperties;
+import org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties;
import org.apereo.cas.configuration.model.support.mfa.simple.CasSimpleMultifactorAuthenticationProperties;
import org.apereo.cas.configuration.model.support.mfa.u2f.U2FMultifactorAuthenticationProperties;
import org.apereo.cas.configuration.model.support.pac4j.Pac4jDelegatedAuthenticationProperties;
@@ -248,17 +249,26 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
final String authModule,
final GoogleMfaAuthModuleConf conf) {
- GoogleAuthenticatorMultifactorProperties props =
+ GoogleAuthenticatorMultifactorProperties gauthProps =
new GoogleAuthenticatorMultifactorProperties();
- props.setName(authModule);
- props.getCore().setIssuer(conf.getIssuer());
- props.getCore().setCodeDigits(conf.getCodeDigits());
- props.getCore().setLabel(conf.getLabel());
- props.getCore().setTimeStepSize(conf.getTimeStepSize());
- props.getCore().setWindowSize(conf.getWindowSize());
+ gauthProps.setName(authModule);
+ gauthProps.getCore().setIssuer(conf.getIssuer());
+ gauthProps.getCore().setCodeDigits(conf.getCodeDigits());
+ gauthProps.getCore().setLabel(conf.getLabel());
+ gauthProps.getCore().setTimeStepSize(conf.getTimeStepSize());
+ gauthProps.getCore().setWindowSize(conf.getWindowSize());
+
+ LdapGoogleAuthenticatorMultifactorProperties ldapProps = new LdapGoogleAuthenticatorMultifactorProperties();
+ ldapProps.setAccountAttributeName(conf.getLdapAccountAttributeName());
+ ldapProps.setBaseDn(conf.getLdapBaseDn());
+ ldapProps.setBindCredential(conf.getLdapBindCredential());
+ ldapProps.setBindDn(conf.getLdapBindDn());
+ ldapProps.setSearchFilter(conf.getLdapSearchFilter());
+ ldapProps.setLdapUrl(conf.getLdapUrl());
+ gauthProps.setLdap(ldapProps);
CasConfigurationProperties casProperties = new CasConfigurationProperties();
- casProperties.getAuthn().getMfa().setGauth(props);
+ casProperties.getAuthn().getMfa().setGauth(gauthProps);
SimpleFilterProvider filterProvider = getParentCasFilterProvider();
filterProvider.addFilter(
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 1e212e5e5e..61d2c2b2df 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -239,6 +239,14 @@ under the License.
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-gauth</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-gauth-ldap</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apereo.cas</groupId>
+ <artifactId>cas-server-support-ldap-core</artifactId>
+ </dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-duo</artifactId>
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
index 3c620392a6..7fbf21233d 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
@@ -24,6 +24,8 @@ import java.time.ZoneId;
import java.util.Date;
import java.util.Map;
import org.apache.syncope.wa.starter.config.SyncopeWARefreshContextJob;
+
+import org.apereo.cas.config.GoogleAuthenticatorLdapConfiguration;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.CasConfigurationPropertiesValidator;
import org.quartz.JobBuilder;
@@ -59,6 +61,18 @@ import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(exclude = {
+ /*
+ List of CAS-specific classes that we want to
+ exclude from auto-configuration. This is required when there is a
+ competing option/implementation available in Syncope that needs to be
+ conditionally activated.
+ */
+ GoogleAuthenticatorLdapConfiguration.class,
+
+ /*
+ List of Spring Boot classes that we want to disable
+ and remove from auto-configuration.
+ */
HibernateJpaAutoConfiguration.class,
JerseyAutoConfiguration.class,
GroovyTemplateAutoConfiguration.class,
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
index a56a98408e..bc70fc2508 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/config/SyncopeWAConfiguration.java
@@ -71,7 +71,9 @@ import org.apereo.cas.adaptors.u2f.storage.U2FDeviceRepository;
import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
import org.apereo.cas.authentication.surrogate.SurrogateAuthenticationService;
import org.apereo.cas.configuration.CasConfigurationProperties;
+import org.apereo.cas.configuration.model.support.mfa.gauth.LdapGoogleAuthenticatorMultifactorProperties;
import org.apereo.cas.configuration.model.support.mfa.u2f.U2FCoreMultifactorAuthenticationProperties;
+import org.apereo.cas.gauth.credential.LdapGoogleAuthenticatorTokenCredentialRepository;
import org.apereo.cas.oidc.jwks.generator.OidcJsonWebKeystoreGeneratorService;
import org.apereo.cas.otp.repository.credentials.OneTimeTokenCredentialRepository;
import org.apereo.cas.otp.repository.token.OneTimeTokenRepository;
@@ -84,15 +86,20 @@ import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerat
import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
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.pac4j.core.client.Client;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+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.ScopedProxyMode;
@Configuration(proxyBeanMethods = false)
public class SyncopeWAConfiguration {
@@ -258,10 +265,27 @@ public class SyncopeWAConfiguration {
restClient, casProperties.getAuthn().getMfa().getGauth().getCore().getTimeStepSize());
}
+ @RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
@Bean
public OneTimeTokenCredentialRepository googleAuthenticatorAccountRegistry(
+ final CasConfigurationProperties casProperties,
+ @Qualifier("googleAuthenticatorAccountCipherExecutor") final CipherExecutor cipherExecutor,
final IGoogleAuthenticator googleAuthenticatorInstance, final WARestClient restClient) {
+ /*
+ Declaring the LDAP-based repository as a Spring bean that would be conditionally activated
+ via properties using annotations is not possible; conditionally-created spring beans cannot be
+ refreshed, which means the settings ever change and the context is refreshed, the repository
+ option can not be re-created. This could be revisited later in CAS 6.6.x using the {@code BeanSupplier}
+ API construct to recreate the same bean in a more conventional way.
+ */
+ LdapGoogleAuthenticatorMultifactorProperties ldap = casProperties.getAuthn().getMfa().getGauth().getLdap();
+ if (StringUtils.isNotBlank(ldap.getBaseDn()) && StringUtils.isNotBlank(ldap.getLdapUrl())
+ && StringUtils.isNotBlank(ldap.getSearchFilter())) {
+ ConnectionFactory connectionFactory = LdapUtils.newLdaptiveConnectionFactory(ldap);
+ return new LdapGoogleAuthenticatorTokenCredentialRepository(cipherExecutor,
+ googleAuthenticatorInstance, connectionFactory, ldap);
+ }
return new SyncopeWAGoogleMfaAuthCredentialRepository(restClient, googleAuthenticatorInstance);
}
diff --git a/wa/starter/src/main/resources/wa.properties b/wa/starter/src/main/resources/wa.properties
index bd29731b54..e2c4c30811 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -37,7 +37,7 @@ spring.web.resources.static-locations=classpath:/thymeleaf/static,classpath:/syn
cas.monitor.endpoints.endpoint.defaults.access=AUTHENTICATED
management.endpoints.enabled-by-default=true
-management.endpoints.web.exposure.include=info,health,loggers,ssoSessions,registeredServices
+management.endpoints.web.exposure.include=info,health,loggers,ssoSessions,registeredServices,refresh
management.endpoint.health.show-details=ALWAYS
spring.cloud.discovery.client.health-indicator.enabled=false