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