From fcc8d539f2f17cff111ef354c5320ac5ba4490c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Thu, 14 Nov 2024 15:02:27 +0100 Subject: [PATCH] [SYNCOPE-1843] Support Support Azure AD authentication and attribute resolution --- .../lib/AbstractAzureActiveDirectoryConf.java | 119 ++++++++++++++++++ .../syncope/common/lib/attr/AttrRepoConf.java | 2 + .../AzureActiveDirectoryAttrRepoConf.java | 46 +++++++ .../common/lib/auth/AuthModuleConf.java | 2 + .../AzureActiveDirectoryAuthModuleConf.java | 52 ++++++++ .../test/resources/core-ojson-test.properties | 1 + .../concepts/attributerepositories.adoc | 1 + .../concepts/authenticationmodules.adoc | 3 +- .../mapping/AttrRepoPropertySourceMapper.java | 21 ++++ .../AuthModulePropertySourceMapper.java | 16 +++ 10 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 common/am/lib/src/main/java/org/apache/syncope/common/lib/AbstractAzureActiveDirectoryConf.java create mode 100644 common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java create mode 100644 common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/AzureActiveDirectoryAuthModuleConf.java 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 00000000000..d0255ed536f --- /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:///}, 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: + * + *
    + *
  • {@code https://login.microsoftonline.com/f31e6716-26e8-4651-b323-2563936b4163}: for a single tenant + * application defined in the tenant
  • + *
  • {@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.
  • + *
  • {@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.
  • + *
  • {@code https://login.microsoftonline.com/common}: in the case of a multi-tenant application, that is an + * application available in several Azure AD tenants.
  • + *
  • 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}.
  • + *
+ */ + 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 18edec4b4e3..682d875f946 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 @@ interface Mapper { Map map(AttrRepoTO attrRepo, JDBCAttrRepoConf conf); Map map(AttrRepoTO attrRepo, SyncopeAttrRepoConf conf); + + Map map(AttrRepoTO attrRepo, AzureActiveDirectoryAttrRepoConf conf); } Map map(AttrRepoTO attrRepo, Mapper mapper); diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java new file mode 100644 index 00000000000..11d657a280d --- /dev/null +++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/attr/AzureActiveDirectoryAttrRepoConf.java @@ -0,0 +1,46 @@ +/* + * 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.attr; + +import java.util.Map; +import org.apache.syncope.common.lib.AbstractAzureActiveDirectoryConf; +import org.apache.syncope.common.lib.to.AttrRepoTO; + +public class AzureActiveDirectoryAttrRepoConf extends AbstractAzureActiveDirectoryConf implements AttrRepoConf { + + private static final long serialVersionUID = -2365294132437794196L; + + /** + * Whether attribute repository should consider the underlying attribute names in a case-insensitive manner. + */ + private boolean caseInsensitive; + + public boolean isCaseInsensitive() { + return caseInsensitive; + } + + public void setCaseInsensitive(final boolean caseInsensitive) { + this.caseInsensitive = caseInsensitive; + } + + @Override + public Map 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 bf416cdc9d5..ac3cc37a51b 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 @@ interface Mapper { Map map(AuthModuleTO authModule, SyncopeAuthModuleConf conf); + Map map(AuthModuleTO authModule, AzureActiveDirectoryAuthModuleConf conf); + Map map(AuthModuleTO authModule, X509AuthModuleConf conf); Map 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 00000000000..5c0933a7462 --- /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:
    + *
  • A regular expression pattern that is tested against the credential identifier.
  • + *
  • A fully qualified class name of your own design that implements {@code Predicate}.
  • + *
  • Path to an external Groovy script that implements the same interface.
  • + *
+ */ + private String credentialCriteria; + + public String getCredentialCriteria() { + return credentialCriteria; + } + + public void setCredentialCriteria(final String credentialCriteria) { + this.credentialCriteria = credentialCriteria; + } + + @Override + public Map map(final AuthModuleTO authModule, final Mapper mapper) { + return mapper.map(authModule, this); + } +} diff --git a/core/persistence-jpa-json/src/test/resources/core-ojson-test.properties b/core/persistence-jpa-json/src/test/resources/core-ojson-test.properties index 96ce2aa12da..bc5d7a97a1e 100644 --- a/core/persistence-jpa-json/src/test/resources/core-ojson-test.properties +++ b/core/persistence-jpa-json/src/test/resources/core-ojson-test.properties @@ -22,6 +22,7 @@ security.secretKey=${secretKey} persistence.domain[0].jdbcURL=jdbc:oracle:thin:@${DB_CONTAINER_IP}:1521/XEPDB1 #persistence.domain[0].jdbcURL=jdbc:oracle:thin:@192.168.0.176:1521/orcl +persistence.domain[0].dbSchema= persistence.domain[0].poolMaxActive=10 persistence.domain[0].poolMinIdle=2 diff --git a/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc b/src/main/asciidoc/reference-guide/concepts/attributerepositories.adoc index 146645e62bb..82f56312f5f 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 0de446d0416..dff10d33506 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 cbdf8bd665c..5c6cf82f1dc 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 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; @@ -33,6 +34,7 @@ import org.apereo.cas.configuration.CasCoreConfigurationUtils; 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; @@ -116,4 +118,23 @@ public Map map(final AttrRepoTO attrRepoTO, final SyncopeAttrRep return prefix("cas.authn.attribute-repository.syncope.", CasCoreConfigurationUtils.asMap(props)); } + + @Override + public Map 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[].", + CasCoreConfigurationUtils.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 b2c43b2d652..fb1a20793b8 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.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; @@ -51,6 +52,7 @@ import org.apache.syncope.wa.bootstrap.WARestClient; import org.apereo.cas.configuration.CasCoreConfigurationUtils; 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; @@ -402,6 +404,20 @@ public Map map(final AuthModuleTO authModuleTO, final SyncopeAut return prefix("cas.authn.syncope.", CasCoreConfigurationUtils.asMap(props)); } + @Override + public Map 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.setClientId(conf.getClientId()); + props.setLoginUrl(conf.getLoginUrl()); + props.setResource(conf.getResource()); + props.setCredentialCriteria(conf.getCredentialCriteria()); + + return prefix("cas.authn.azure-active-directory.", CasCoreConfigurationUtils.asMap(props)); + } + @Override public Map map(final AuthModuleTO authModuleTO, final GoogleMfaAuthModuleConf conf) { GoogleAuthenticatorMultifactorProperties props = new GoogleAuthenticatorMultifactorProperties();