You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2024/11/14 14:11:33 UTC

(syncope) 01/02: [SYNCOPE-1843] Support Support Azure AD authentication and attribute resolution

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 c00465084db463218b8da440d0709f1118ca73de
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Thu Nov 14 15:02:27 2024 +0100

    [SYNCOPE-1843] Support Support Azure AD authentication and attribute resolution
---
 .../lib/AbstractAzureActiveDirectoryConf.java      | 119 +++++++++++++++++++++
 .../syncope/common/lib/attr/AttrRepoConf.java      |   2 +
 ....java => AzureActiveDirectoryAttrRepoConf.java} |  27 +++--
 .../syncope/common/lib/auth/AuthModuleConf.java    |   2 +
 .../auth/AzureActiveDirectoryAuthModuleConf.java   |  52 +++++++++
 .../concepts/attributerepositories.adoc            |   1 +
 .../concepts/authenticationmodules.adoc            |   3 +-
 .../mapping/AttrRepoPropertySourceMapper.java      |  19 ++++
 .../mapping/AuthModulePropertySourceMapper.java    |  19 ++++
 9 files changed, 232 insertions(+), 12 deletions(-)

diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/AbstractAzureActiveDirectoryConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/AbstractAzureActiveDirectoryConf.java
new file mode 100644
index 0000000000..d0255ed536
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/AbstractAzureActiveDirectoryConf.java
@@ -0,0 +1,119 @@
+/*
+ * 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.common.lib;
+
+import java.io.Serializable;
+
+public abstract class AbstractAzureActiveDirectoryConf implements Serializable {
+
+    private static final long serialVersionUID = 282571926999684266L;
+
+    private String clientId;
+
+    private String clientSecret;
+
+    /**
+     * This URL of the security token service that CAS goes to for acquiring tokens for resources and users.
+     * This URL allows CAS to establish what is called an 'authority'.
+     * You can think of the authority as the directory issuing the identities/tokens. The login URL here is then
+     * composed of {@code https://<instance>/<tenant>}, where 'instance' is the Azure AD host
+     * (such as {@code https://login.microsoftonline.com}) and 'tenant' is the domain name
+     * (such as {@code contoso.onmicrosoft.com}) or tenant ID of the directory.
+     * Examples of authority URL are:
+     *
+     * <ul>
+     * <li>{@code https://login.microsoftonline.com/f31e6716-26e8-4651-b323-2563936b4163}: for a single tenant
+     * application defined in the tenant</li>
+     * <li>{@code https://login.microsoftonline.com/contoso.onmicrosoft.com}: This representation is like the previous
+     * one, but uses the tenant domain name instead of the tenant Id.</li>
+     * <li>{@code https://login.microsoftonline.de/contoso.de}: also uses a domain name, but in this case the Azure AD
+     * tenant admins have set a custom domain for their tenant, and the
+     * instance URL here is for the German national cloud.</li>
+     * <li>{@code https://login.microsoftonline.com/common}: in the case of a multi-tenant application, that is an
+     * application available in several Azure AD tenants.</li>
+     * <li>It can finally be an Active Directory Federation Services (ADFS) URL, which is recognized
+     * with the convention that the URL should contain adfs like {@code https://contoso.com/adfs}.</li>
+     * </ul>
+     */
+    private String loginUrl = "https://login.microsoftonline.com/common/";
+
+    /**
+     * Resource url for the graph API to fetch attributes.
+     */
+    private String resource = "https://graph.microsoft.com/";
+
+    /**
+     * The microsoft tenant id.
+     */
+    private String tenant;
+
+    /**
+     * Scope used when fetching access tokens.
+     * Multiple scopes may be separated using a comma.
+     */
+    private String scope = "openid,email,profile,address";
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(final String clientId) {
+        this.clientId = clientId;
+    }
+
+    public String getClientSecret() {
+        return clientSecret;
+    }
+
+    public void setClientSecret(final String clientSecret) {
+        this.clientSecret = clientSecret;
+    }
+
+    public String getLoginUrl() {
+        return loginUrl;
+    }
+
+    public void setLoginUrl(final String loginUrl) {
+        this.loginUrl = loginUrl;
+    }
+
+    public String getResource() {
+        return resource;
+    }
+
+    public void setResource(final String resource) {
+        this.resource = resource;
+    }
+
+    public String getTenant() {
+        return tenant;
+    }
+
+    public void setTenant(final String tenant) {
+        this.tenant = tenant;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(final String scope) {
+        this.scope = scope;
+    }
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java
index 18edec4b4e..682d875f94 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java
@@ -35,6 +35,8 @@ public interface AttrRepoConf extends BaseBean {
         Map<String, Object> map(AttrRepoTO attrRepo, JDBCAttrRepoConf conf);
 
         Map<String, Object> map(AttrRepoTO attrRepo, SyncopeAttrRepoConf conf);
+
+        Map<String, Object> map(AttrRepoTO attrRepo, AzureActiveDirectoryAttrRepoConf conf);
     }
 
     Map<String, Object> map(AttrRepoTO attrRepo, Mapper mapper);
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java
similarity index 55%
copy from common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java
copy to common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java
index 18edec4b4e..11d657a280 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AttrRepoConf.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java
@@ -18,24 +18,29 @@
  */
 package org.apache.syncope.common.lib.attr;
 
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import java.util.Map;
-import org.apache.syncope.common.lib.BaseBean;
+import org.apache.syncope.common.lib.AbstractAzureActiveDirectoryConf;
 import org.apache.syncope.common.lib.to.AttrRepoTO;
 
-@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "_class")
-public interface AttrRepoConf extends BaseBean {
+public class AzureActiveDirectoryAttrRepoConf extends AbstractAzureActiveDirectoryConf implements AttrRepoConf {
 
-    interface Mapper {
+    private static final long serialVersionUID = -2365294132437794196L;
 
-        Map<String, Object> map(AttrRepoTO attrRepo, StubAttrRepoConf conf);
+    /**
+     * Whether attribute repository should consider the underlying attribute names in a case-insensitive manner.
+     */
+    private boolean caseInsensitive;
 
-        Map<String, Object> map(AttrRepoTO attrRepo, LDAPAttrRepoConf conf);
-
-        Map<String, Object> map(AttrRepoTO attrRepo, JDBCAttrRepoConf conf);
+    public boolean isCaseInsensitive() {
+        return caseInsensitive;
+    }
 
-        Map<String, Object> map(AttrRepoTO attrRepo, SyncopeAttrRepoConf conf);
+    public void setCaseInsensitive(final boolean caseInsensitive) {
+        this.caseInsensitive = caseInsensitive;
     }
 
-    Map<String, Object> map(AttrRepoTO attrRepo, Mapper mapper);
+    @Override
+    public Map<String, Object> map(final AttrRepoTO attrRepo, final Mapper mapper) {
+        return mapper.map(attrRepo, this);
+    }
 }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AuthModuleConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AuthModuleConf.java
index 1f20f073df..265b4b457b 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AuthModuleConf.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AuthModuleConf.java
@@ -52,6 +52,8 @@ public interface AuthModuleConf extends BaseBean {
 
         Map<String, Object> map(AuthModuleTO authModule, SyncopeAuthModuleConf conf);
 
+        Map<String, Object> map(AuthModuleTO authModule, AzureActiveDirectoryAuthModuleConf conf);
+
         Map<String, Object> map(AuthModuleTO authModule, X509AuthModuleConf conf);
 
         Map<String, Object> map(AuthModuleTO authModule, GoogleMfaAuthModuleConf conf);
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AzureActiveDirectoryAuthModuleConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AzureActiveDirectoryAuthModuleConf.java
new file mode 100644
index 0000000000..5c0933a746
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AzureActiveDirectoryAuthModuleConf.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.lib.auth;
+
+import java.util.Map;
+import org.apache.syncope.common.lib.AbstractAzureActiveDirectoryConf;
+import org.apache.syncope.common.lib.to.AuthModuleTO;
+
+public class AzureActiveDirectoryAuthModuleConf extends AbstractAzureActiveDirectoryConf implements AuthModuleConf {
+
+    private static final long serialVersionUID = 6053163884651768614L;
+
+    /**
+     * A number of authentication handlers are allowed to determine whether they can operate on the provided credential
+     * and as such lend themselves to be tried and tested during the authentication handler selection phase.
+     * The credential criteria may be one of the following options:<ul>
+     * <li>A regular expression pattern that is tested against the credential identifier.</li>
+     * <li>A fully qualified class name of your own design that implements {@code Predicate}.</li>
+     * <li>Path to an external Groovy script that implements the same interface.</li>
+     * </ul>
+     */
+    private String credentialCriteria;
+
+    public String getCredentialCriteria() {
+        return credentialCriteria;
+    }
+
+    public void setCredentialCriteria(final String credentialCriteria) {
+        this.credentialCriteria = credentialCriteria;
+    }
+
+    @Override
+    public Map<String, Object> map(final AuthModuleTO authModule, final Mapper mapper) {
+        return mapper.map(authModule, this);
+    }
+}
diff --git a/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc b/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc
index c8299a7137..26da7b278f 100644
--- a/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc
@@ -27,6 +27,7 @@ Some attribute repositories are provided:
 * https://apereo.github.io/cas/6.6.x/integration/Attribute-Resolution-LDAP.html[LDAP^]
 * https://apereo.github.io/cas/6.6.x/integration/Attribute-Resolution-Stub.html[Stub^]
 * https://apereo.github.io/cas/6.6.x/integration/Attribute-Resolution-Syncope.html[Syncope^]
+* https://apereo.github.io/cas/6.6.x/integration/Attribute-Resolution-AzureAD.html[Azure Active Directory^]
 
 [TIP]
 ====
diff --git a/src/main/asciidoc/reference-guide/concepts/authenticationmodules.adoc b/src/main/asciidoc/reference-guide/concepts/authenticationmodules.adoc
index f99e87a2e6..e45ea041d2 100644
--- a/src/main/asciidoc/reference-guide/concepts/authenticationmodules.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/authenticationmodules.adoc
@@ -29,12 +29,13 @@ Several authentication modules are provided:
     ** https://apereo.github.io/cas/6.6.x/authentication/LDAP-Authentication.html[LDAP^]
     ** https://apereo.github.io/cas/6.6.x/authentication/SPNEGO-Authentication.html[SPNEGO^]
     ** https://apereo.github.io/cas/6.6.x/authentication/Syncope-Authentication.html[Syncope^]
+    ** https://apereo.github.io/cas/6.6.x/authentication/Azure-ActiveDirectory-Authentication.html[Azure Active Directory^]
     ** https://apereo.github.io/cas/6.6.x/authentication/X509-Authentication.html[X509^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Generic-OpenID-Connect.html[OpenID Connect^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-OAuth20.html[OAuth2^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-SAML.htmll[SAML^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Apple.html[Apple Signin^]
-    ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Azure-AD.html[Azure Active Directory^]
+    ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Azure-AD.html[Azure Active Directory (OIDC)^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Google-OpenID-Connect.html[Google OpenID^]
     ** https://apereo.github.io/cas/6.6.x/integration/Delegate-Authentication-Keycloak.html[Keycloak^]
 * MFA:
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AttrRepoPropertySourceMapper.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AttrRepoPropertySourceMapper.java
index b24ef8d064..a42880a42b 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AttrRepoPropertySourceMapper.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AttrRepoPropertySourceMapper.java
@@ -23,6 +23,7 @@ import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.attr.AttrRepoConf;
+import org.apache.syncope.common.lib.attr.AzureActiveDirectoryAttrRepoConf;
 import org.apache.syncope.common.lib.attr.JDBCAttrRepoConf;
 import org.apache.syncope.common.lib.attr.LDAPAttrRepoConf;
 import org.apache.syncope.common.lib.attr.StubAttrRepoConf;
@@ -32,6 +33,7 @@ import org.apache.syncope.common.lib.to.Item;
 import org.apache.syncope.wa.bootstrap.WARestClient;
 import org.apereo.cas.configuration.model.core.authentication.AttributeRepositoryStates;
 import org.apereo.cas.configuration.model.core.authentication.StubPrincipalAttributesProperties;
+import org.apereo.cas.configuration.model.support.azuread.AzureActiveDirectoryAttributesProperties;
 import org.apereo.cas.configuration.model.support.jdbc.JdbcPrincipalAttributesProperties;
 import org.apereo.cas.configuration.model.support.ldap.LdapPrincipalAttributesProperties;
 import org.apereo.cas.configuration.model.support.syncope.SyncopePrincipalAttributesProperties;
@@ -115,4 +117,21 @@ public class AttrRepoPropertySourceMapper extends PropertySourceMapper implement
 
         return prefix("cas.authn.attribute-repository.syncope.", WAConfUtils.asMap(props));
     }
+
+    @Override
+    public Map<String, Object> map(final AttrRepoTO attrRepoTO, final AzureActiveDirectoryAttrRepoConf conf) {
+        AzureActiveDirectoryAttributesProperties props = new AzureActiveDirectoryAttributesProperties();
+        props.setId(attrRepoTO.getKey());
+        props.setOrder(attrRepoTO.getOrder());
+        props.setClientId(conf.getClientId());
+        props.setClientSecret(conf.getClientSecret());
+        props.setLoginBaseUrl(conf.getLoginUrl());
+        props.setResource(conf.getResource());
+        props.setTenant(conf.getTenant());
+        props.setScope(conf.getScope());
+        props.setCaseInsensitive(conf.isCaseInsensitive());
+        props.setAttributes(attrRepoTO.getItems().stream().map(Item::getExtAttrName).collect(Collectors.joining(",")));
+
+        return prefix("cas.authn.attribute-repository.azure-active-directory[].", WAConfUtils.asMap(props));
+    }
 }
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AuthModulePropertySourceMapper.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AuthModulePropertySourceMapper.java
index a975bc4ad3..36a0f061b7 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AuthModulePropertySourceMapper.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/mapping/AuthModulePropertySourceMapper.java
@@ -28,6 +28,7 @@ import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.auth.AbstractOIDCAuthModuleConf;
 import org.apache.syncope.common.lib.auth.AppleOIDCAuthModuleConf;
 import org.apache.syncope.common.lib.auth.AuthModuleConf;
+import org.apache.syncope.common.lib.auth.AzureActiveDirectoryAuthModuleConf;
 import org.apache.syncope.common.lib.auth.AzureOIDCAuthModuleConf;
 import org.apache.syncope.common.lib.auth.DuoMfaAuthModuleConf;
 import org.apache.syncope.common.lib.auth.GoogleMfaAuthModuleConf;
@@ -49,6 +50,7 @@ import org.apache.syncope.common.lib.to.Item;
 import org.apache.syncope.common.lib.types.AuthModuleState;
 import org.apache.syncope.wa.bootstrap.WARestClient;
 import org.apereo.cas.configuration.model.core.authentication.AuthenticationHandlerStates;
+import org.apereo.cas.configuration.model.support.azuread.AzureActiveDirectoryAuthenticationProperties;
 import org.apereo.cas.configuration.model.support.generic.AcceptAuthenticationProperties;
 import org.apereo.cas.configuration.model.support.jaas.JaasAuthenticationProperties;
 import org.apereo.cas.configuration.model.support.jdbc.authn.QueryJdbcAuthenticationProperties;
@@ -394,6 +396,23 @@ public class AuthModulePropertySourceMapper extends PropertySourceMapper impleme
         return prefix("cas.authn.syncope.", WAConfUtils.asMap(props));
     }
 
+    @Override
+    public Map<String, Object> map(final AuthModuleTO authModuleTO, final AzureActiveDirectoryAuthModuleConf conf) {
+        AzureActiveDirectoryAuthenticationProperties props = new AzureActiveDirectoryAuthenticationProperties();
+        props.setName(authModuleTO.getKey());
+        props.setOrder(authModuleTO.getOrder());
+        props.setState(AuthenticationHandlerStates.valueOf(authModuleTO.getState().name()));
+        props.setLoginUrl(conf.getLoginUrl());
+        props.setResource(conf.getResource());
+        props.setClientId(conf.getClientId());
+        props.setClientSecret(conf.getClientSecret());
+        props.setTenant(conf.getTenant());
+        props.setScope(conf.getScope());
+        props.setCredentialCriteria(conf.getCredentialCriteria());
+
+        return prefix("cas.authn.azure-active-directory.", WAConfUtils.asMap(props));
+    }
+
     @Override
     public Map<String, Object> map(final AuthModuleTO authModuleTO, final GoogleMfaAuthModuleConf conf) {
         GoogleAuthenticatorMultifactorProperties props = new GoogleAuthenticatorMultifactorProperties();