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 2017/03/31 13:43:39 UTC
[02/15] syncope git commit: [SYNCOPE-1041] SAML 2.0 SP extension:
core components
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2LoginResponseTO.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2LoginResponseTO.java b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2LoginResponseTO.java
new file mode 100644
index 0000000..b6ece53
--- /dev/null
+++ b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2LoginResponseTO.java
@@ -0,0 +1,120 @@
+/*
+ * 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.to;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import org.apache.syncope.common.lib.AbstractBaseBean;
+
+@XmlRootElement(name = "saml2LoginResponse")
+@XmlType
+public class SAML2LoginResponseTO extends AbstractBaseBean {
+
+ private static final long serialVersionUID = 794772343787258010L;
+
+ private String nameID;
+
+ private String sessionIndex;
+
+ private Date authInstant;
+
+ private Date notOnOrAfter;
+
+ private String accessToken;
+
+ private String username;
+
+ private final Set<AttrTO> attrs = new HashSet<>();
+
+ public String getNameID() {
+ return nameID;
+ }
+
+ public void setNameID(final String nameID) {
+ this.nameID = nameID;
+ }
+
+ public String getSessionIndex() {
+ return sessionIndex;
+ }
+
+ public void setSessionIndex(final String sessionIndex) {
+ this.sessionIndex = sessionIndex;
+ }
+
+ public Date getAuthInstant() {
+ if (authInstant != null) {
+ return new Date(authInstant.getTime());
+ }
+ return null;
+ }
+
+ public void setAuthInstant(final Date authInstant) {
+ if (authInstant != null) {
+ this.authInstant = new Date(authInstant.getTime());
+ } else {
+ this.authInstant = null;
+ }
+ }
+
+ public Date getNotOnOrAfter() {
+ if (notOnOrAfter != null) {
+ return new Date(notOnOrAfter.getTime());
+ }
+ return null;
+ }
+
+ public void setNotOnOrAfter(final Date notOnOrAfter) {
+ if (notOnOrAfter != null) {
+ this.notOnOrAfter = new Date(notOnOrAfter.getTime());
+ } else {
+ this.notOnOrAfter = null;
+ }
+ }
+
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void setAccessToken(final String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(final String username) {
+ this.username = username;
+ }
+
+ @XmlElementWrapper(name = "attrs")
+ @XmlElement(name = "attr")
+ @JsonProperty("attrs")
+ public Set<AttrTO> getAttrs() {
+ return attrs;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2RequestTO.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2RequestTO.java b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2RequestTO.java
new file mode 100644
index 0000000..136b58e
--- /dev/null
+++ b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/to/SAML2RequestTO.java
@@ -0,0 +1,61 @@
+/*
+ * 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.to;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import org.apache.syncope.common.lib.AbstractBaseBean;
+
+@XmlRootElement(name = "saml2request")
+@XmlType
+public class SAML2RequestTO extends AbstractBaseBean {
+
+ private static final long serialVersionUID = -2454209295007372086L;
+
+ private String idpServiceAddress;
+
+ private String content;
+
+ private String relayState;
+
+ public String getIdpServiceAddress() {
+ return idpServiceAddress;
+ }
+
+ public void setIdpServiceAddress(final String idpServiceAddress) {
+ this.idpServiceAddress = idpServiceAddress;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(final String content) {
+ this.content = content;
+ }
+
+ public String getRelayState() {
+ return relayState;
+ }
+
+ public void setRelayState(final String relayState) {
+ this.relayState = relayState;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPEntitlement.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPEntitlement.java b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPEntitlement.java
new file mode 100644
index 0000000..985bf8b
--- /dev/null
+++ b/ext/saml2sp/common-lib/src/main/java/org/apache/syncope/common/lib/types/SAML2SPEntitlement.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.common.lib.types;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.Set;
+import java.util.TreeSet;
+
+public final class SAML2SPEntitlement {
+
+ public static final String IDP_READ = "IDP_READ";
+
+ public static final String IDP_LIST = "IDP_LIST";
+
+ public static final String IDP_IMPORT = "IDP_IMPORT";
+
+ public static final String IDP_UPDATE = "IDP_UPDATE";
+
+ public static final String IDP_DELETE = "IDP_DELETE";
+
+ private static final Set<String> VALUES;
+
+ static {
+ Set<String> values = new TreeSet<>();
+ for (Field field : SAML2SPEntitlement.class.getDeclaredFields()) {
+ if (Modifier.isStatic(field.getModifiers()) && String.class.equals(field.getType())) {
+ values.add(field.getName());
+ }
+ }
+ VALUES = Collections.unmodifiableSet(values);
+ }
+
+ public static Set<String> values() {
+ return VALUES;
+ }
+
+ private SAML2SPEntitlement() {
+ // private constructor for static utility class
+ }
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/pom.xml
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/pom.xml b/ext/saml2sp/logic/pom.xml
new file mode 100644
index 0000000..54500a8
--- /dev/null
+++ b/ext/saml2sp/logic/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.syncope.ext</groupId>
+ <artifactId>syncope-ext-saml2sp</artifactId>
+ <version>2.1.0-SNAPSHOT</version>
+ </parent>
+
+ <name>Apache Syncope Extensions: SAML 2.0 SP Logic</name>
+ <description>Apache Syncope Extensions: SAML 2.0 SP Logic</description>
+ <groupId>org.apache.syncope.ext.saml2sp</groupId>
+ <artifactId>syncope-ext-saml2sp-logic</artifactId>
+ <packaging>jar</packaging>
+
+ <properties>
+ <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.syncope.core</groupId>
+ <artifactId>syncope-core-logic</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.syncope.ext.saml2sp</groupId>
+ <artifactId>syncope-ext-saml2sp-provisioning-java</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.cxf</groupId>
+ <artifactId>cxf-rt-rs-security-sso-saml</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.wss4j</groupId>
+ <artifactId>wss4j-ws-security-dom</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-saml-impl</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+</project>
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPLogic.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPLogic.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPLogic.java
new file mode 100644
index 0000000..3f6b4a3
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2IdPLogic.java
@@ -0,0 +1,226 @@
+/*
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.Transformer;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.MappingItemTO;
+import org.apache.syncope.common.lib.to.SAML2IdPTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.SAML2SPEntitlement;
+import org.apache.syncope.core.logic.saml2.SAML2ReaderWriter;
+import org.apache.syncope.core.logic.saml2.SAML2IdPCache;
+import org.apache.syncope.core.logic.saml2.SAML2IdPEntity;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.SAML2IdPDAO;
+import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
+import org.apache.syncope.core.provisioning.api.data.SAML2IdPDataBinder;
+import org.apache.wss4j.common.saml.OpenSAMLUtil;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+@Component
+public class SAML2IdPLogic extends AbstractTransactionalLogic<SAML2IdPTO> {
+
+ static {
+ OpenSAMLUtil.initSamlEngine(false);
+ }
+
+ @Autowired
+ private SAML2IdPCache cache;
+
+ @Autowired
+ private SAML2IdPDataBinder binder;
+
+ @Autowired
+ private SAML2IdPDAO idpDAO;
+
+ @Autowired
+ private SAML2ReaderWriter saml2rw;
+
+ @PreAuthorize("hasRole('" + SAML2SPEntitlement.IDP_LIST + "')")
+ @Transactional(readOnly = true)
+ public List<SAML2IdPTO> list() {
+ return CollectionUtils.collect(idpDAO.findAll(), new Transformer<SAML2IdP, SAML2IdPTO>() {
+
+ @Override
+ public SAML2IdPTO transform(final SAML2IdP input) {
+ return binder.getIdPTO(input);
+ }
+ }, new ArrayList<SAML2IdPTO>());
+ }
+
+ @PreAuthorize("hasRole('" + SAML2SPEntitlement.IDP_READ + "')")
+ @Transactional(readOnly = true)
+ public SAML2IdPTO read(final String key) {
+ SAML2IdP idp = idpDAO.find(key);
+ if (idp == null) {
+ throw new NotFoundException("SAML 2.0 IdP '" + key + "'");
+ }
+
+ return binder.getIdPTO(idp);
+ }
+
+ private List<SAML2IdPTO> importIdPs(final InputStream input) throws Exception {
+ List<EntityDescriptor> idpEntityDescriptors = new ArrayList<>();
+
+ Element root = OpenSAMLUtil.getParserPool().parse(new InputStreamReader(input)).getDocumentElement();
+ if (SAMLConstants.SAML20MD_NS.equals(root.getNamespaceURI())
+ && EntityDescriptor.DEFAULT_ELEMENT_LOCAL_NAME.equals(root.getLocalName())) {
+
+ idpEntityDescriptors.add((EntityDescriptor) OpenSAMLUtil.fromDom(root));
+ } else if (SAMLConstants.SAML20MD_NS.equals(root.getNamespaceURI())
+ && EntitiesDescriptor.DEFAULT_ELEMENT_LOCAL_NAME.equals(root.getLocalName())) {
+
+ NodeList children = root.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ if (SAMLConstants.SAML20MD_NS.equals(child.getNamespaceURI())
+ && EntityDescriptor.DEFAULT_ELEMENT_LOCAL_NAME.equals(child.getLocalName())) {
+
+ NodeList descendants = child.getChildNodes();
+ for (int j = 0; j < descendants.getLength(); j++) {
+ Node descendant = descendants.item(j);
+ if (SAMLConstants.SAML20MD_NS.equals(descendant.getNamespaceURI())
+ && IDPSSODescriptor.DEFAULT_ELEMENT_LOCAL_NAME.equals(descendant.getLocalName())) {
+
+ idpEntityDescriptors.add((EntityDescriptor) OpenSAMLUtil.fromDom((Element) child));
+ }
+ }
+ }
+ }
+ }
+
+ List<SAML2IdPTO> result = new ArrayList<>(idpEntityDescriptors.size());
+ for (EntityDescriptor idpEntityDescriptor : idpEntityDescriptors) {
+ SAML2IdPTO idpTO = new SAML2IdPTO();
+ idpTO.setEntityID(idpEntityDescriptor.getEntityID());
+ idpTO.setUseDeflateEncoding(false);
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ saml2rw.write(new OutputStreamWriter(baos), idpEntityDescriptor, false);
+ idpTO.setMetadata(Base64.encodeBase64String(baos.toByteArray()));
+ }
+ MappingItemTO connObjectKeyItem = new MappingItemTO();
+ connObjectKeyItem.setIntAttrName("username");
+ connObjectKeyItem.setExtAttrName("NameID");
+ idpTO.setConnObjectKeyItem(connObjectKeyItem);
+ result.add(idpTO);
+
+ cache.put(idpEntityDescriptor, connObjectKeyItem, false);
+ }
+
+ return result;
+ }
+
+ @PreAuthorize("hasRole('" + SAML2SPEntitlement.IDP_IMPORT + "')")
+ public List<String> importFromMetadata(final InputStream input) {
+ List<String> imported = new ArrayList<>();
+
+ try {
+ for (SAML2IdPTO idpTO : importIdPs(input)) {
+ SAML2IdP idp = idpDAO.save(binder.create(idpTO));
+ imported.add(idp.getKey());
+ }
+ } catch (SyncopeClientException e) {
+ throw e;
+ } catch (Exception e) {
+ LOG.error("Unexpected error while importing IdP metadata", e);
+ SyncopeClientException ex = SyncopeClientException.build(ClientExceptionType.InvalidEntity);
+ ex.getElements().add(e.getMessage());
+ throw ex;
+ }
+
+ return imported;
+ }
+
+ @PreAuthorize("hasRole('" + SAML2SPEntitlement.IDP_UPDATE + "')")
+ public void update(final SAML2IdPTO saml2IdpTO) {
+ SAML2IdP saml2Idp = idpDAO.find(saml2IdpTO.getKey());
+ if (saml2Idp == null) {
+ throw new NotFoundException("SAML 2.0 IdP '" + saml2IdpTO.getKey() + "'");
+ }
+
+ saml2Idp = idpDAO.save(binder.update(saml2Idp, saml2IdpTO));
+
+ SAML2IdPEntity idpEntity = cache.get(saml2Idp.getEntityID());
+ if (idpEntity != null) {
+ idpEntity.setUseDeflateEncoding(saml2Idp.isUseDeflateEncoding());
+ idpEntity.setConnObjectKeyItem(binder.getIdPTO(saml2Idp).getConnObjectKeyItem());
+ }
+ }
+
+ @PreAuthorize("hasRole('" + SAML2SPEntitlement.IDP_DELETE + "')")
+ public void delete(final String key) {
+ SAML2IdP idp = idpDAO.find(key);
+ if (idp == null) {
+ throw new NotFoundException("SAML 2.0 IdP '" + key + "'");
+ }
+
+ idpDAO.delete(key);
+ cache.remove(idp.getEntityID());
+ }
+
+ @Override
+ protected SAML2IdPTO resolveReference(final Method method, final Object... args)
+ throws UnresolvedReferenceException {
+
+ String key = null;
+
+ if (ArrayUtils.isNotEmpty(args)) {
+ for (int i = 0; key == null && i < args.length; i++) {
+ if (args[i] instanceof String) {
+ key = (String) args[i];
+ } else if (args[i] instanceof SAML2IdPTO) {
+ key = ((SAML2IdPTO) args[i]).getKey();
+ }
+ }
+ }
+
+ if (key != null) {
+ try {
+ return binder.getIdPTO(idpDAO.find(key));
+ } catch (Throwable ignore) {
+ LOG.debug("Unresolved reference", ignore);
+ throw new UnresolvedReferenceException(ignore);
+ }
+ }
+
+ throw new UnresolvedReferenceException();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPLogic.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPLogic.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPLogic.java
new file mode 100644
index 0000000..f452208
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPLogic.java
@@ -0,0 +1,689 @@
+/*
+ * 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;
+
+import com.fasterxml.uuid.Generators;
+import com.fasterxml.uuid.impl.RandomBasedGenerator;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Method;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.MultivaluedMap;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.lang3.tuple.Triple;
+import org.apache.cxf.helpers.IOUtils;
+import org.apache.cxf.jaxrs.utils.JAXRSUtils;
+import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
+import org.apache.cxf.rs.security.jose.jws.JwsSignatureVerifier;
+import org.apache.cxf.rs.security.saml.sso.SSOConstants;
+import org.apache.syncope.common.lib.AbstractBaseBean;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.SAML2RequestTO;
+import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
+import org.apache.syncope.common.lib.to.MappingItemTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.syncope.core.logic.init.SAML2SPLoader;
+import org.apache.syncope.core.logic.saml2.SAML2ReaderWriter;
+import org.apache.syncope.core.logic.saml2.SAML2IdPCache;
+import org.apache.syncope.core.logic.saml2.SAML2IdPEntity;
+import org.apache.syncope.core.logic.saml2.SAML2Signer;
+import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
+import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.SAML2IdPDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
+import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.IntAttrName;
+import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
+import org.apache.syncope.core.provisioning.api.data.MappingItemTransformer;
+import org.apache.syncope.core.provisioning.api.utils.EntityUtils;
+import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.apache.wss4j.common.saml.OpenSAMLUtil;
+import org.joda.time.DateTime;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.saml.common.SAMLVersion;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.AuthnContext;
+import org.opensaml.saml.saml2.core.AuthnContextClassRef;
+import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.opensaml.saml.saml2.core.AuthnStatement;
+import org.opensaml.saml.saml2.core.Issuer;
+import org.opensaml.saml.saml2.core.LogoutRequest;
+import org.opensaml.saml.saml2.core.LogoutResponse;
+import org.opensaml.saml.saml2.core.NameID;
+import org.opensaml.saml.saml2.core.NameIDPolicy;
+import org.opensaml.saml.saml2.core.NameIDType;
+import org.opensaml.saml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.SessionIndex;
+import org.opensaml.saml.saml2.core.StatusCode;
+import org.opensaml.saml.saml2.core.impl.AuthnContextClassRefBuilder;
+import org.opensaml.saml.saml2.core.impl.AuthnRequestBuilder;
+import org.opensaml.saml.saml2.core.impl.IssuerBuilder;
+import org.opensaml.saml.saml2.core.impl.LogoutRequestBuilder;
+import org.opensaml.saml.saml2.core.impl.NameIDBuilder;
+import org.opensaml.saml.saml2.core.impl.NameIDPolicyBuilder;
+import org.opensaml.saml.saml2.core.impl.RequestedAuthnContextBuilder;
+import org.opensaml.saml.saml2.core.impl.SessionIndexBuilder;
+import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.KeyDescriptor;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.SingleLogoutService;
+import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
+import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorBuilder;
+import org.opensaml.saml.saml2.metadata.impl.KeyDescriptorBuilder;
+import org.opensaml.saml.saml2.metadata.impl.NameIDFormatBuilder;
+import org.opensaml.saml.saml2.metadata.impl.SPSSODescriptorBuilder;
+import org.opensaml.saml.saml2.metadata.impl.SingleLogoutServiceBuilder;
+import org.opensaml.xmlsec.keyinfo.KeyInfoGenerator;
+import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SAML2SPLogic extends AbstractTransactionalLogic<AbstractBaseBean> {
+
+ private static final Integer JWT_RELAY_STATE_DURATION = 5;
+
+ private static final String JWT_CLAIM_IDP_DEFLATE = "IDP_DEFLATE";
+
+ private static final String JWT_CLAIM_IDP_ENTITYID = "IDP_ENTITYID";
+
+ private static final String JWT_CLAIM_NAMEID_FORMAT = "NAMEID_FORMAT";
+
+ private static final String JWT_CLAIM_NAMEID_VALUE = "NAMEID_VALUE";
+
+ private static final String JWT_CLAIM_SESSIONINDEX = "SESSIONINDEX";
+
+ private static final RandomBasedGenerator UUID_GENERATOR = Generators.randomBasedGenerator();
+
+ static {
+ OpenSAMLUtil.initSamlEngine(false);
+ }
+
+ @Autowired
+ private JwsSignatureVerifier jwsSignatureCerifier;
+
+ @Autowired
+ private AccessTokenDataBinder accessTokenDataBinder;
+
+ @Autowired
+ private SAML2SPLoader loader;
+
+ @Autowired
+ private SAML2IdPCache cache;
+
+ @Autowired
+ private UserDAO userDAO;
+
+ @Autowired
+ private SAML2IdPDAO saml2IdPDAO;
+
+ @Autowired
+ private PlainSchemaDAO plainSchemaDAO;
+
+ @Autowired
+ private AccessTokenDAO accessTokenDAO;
+
+ @Autowired
+ private IntAttrNameParser intAttrNameParser;
+
+ @Autowired
+ private EntityFactory entityFactory;
+
+ @Autowired
+ private SAML2ReaderWriter saml2rw;
+
+ @Autowired
+ private SAML2Signer saml2Signer;
+
+ @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
+ public void getMetadata(final String spEntityID, final OutputStream os) {
+ try {
+ EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
+ spEntityDescriptor.setEntityID(spEntityID);
+
+ SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
+ spSSODescriptor.setWantAssertionsSigned(true);
+ spSSODescriptor.setAuthnRequestsSigned(true);
+
+ X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
+ keyInfoGeneratorFactory.setEmitEntityCertificate(true);
+ KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
+ keyInfoGenerator.generate(loader.getCredential());
+
+ KeyDescriptor keyDescriptor = new KeyDescriptorBuilder().buildObject();
+ keyDescriptor.setKeyInfo(keyInfoGenerator.generate(loader.getCredential()));
+ spSSODescriptor.getKeyDescriptors().add(keyDescriptor);
+
+ SingleLogoutService singleLogoutService = new SingleLogoutServiceBuilder().buildObject();
+ singleLogoutService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
+ singleLogoutService.setLocation(spEntityID + "saml2sp/logout");
+ singleLogoutService.setResponseLocation(spEntityID + "saml2sp/logout");
+ spSSODescriptor.getSingleLogoutServices().add(singleLogoutService);
+
+ NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
+ nameIDFormat.setFormat(NameIDType.PERSISTENT);
+ spSSODescriptor.getNameIDFormats().add(nameIDFormat);
+ nameIDFormat = new NameIDFormatBuilder().buildObject();
+ nameIDFormat.setFormat(NameIDType.TRANSIENT);
+ spSSODescriptor.getNameIDFormats().add(nameIDFormat);
+
+ AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
+ assertionConsumerService.setIndex(0);
+ assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
+ assertionConsumerService.setLocation(spEntityID + "saml2sp/assertion-consumer");
+
+ spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
+ spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
+
+ spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
+
+ saml2rw.write(new OutputStreamWriter(os), spEntityDescriptor, true);
+ } catch (Exception e) {
+ LOG.error("While getting SP metadata", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+ }
+
+ private SAML2IdPEntity getIdP(final String entityID) {
+ SAML2IdPEntity idp = null;
+
+ SAML2IdP saml2IdP = saml2IdPDAO.findByEntityID(entityID);
+ if (saml2IdP != null) {
+ try {
+ idp = cache.put(saml2IdP);
+ } catch (Exception e) {
+ LOG.error("Could not build SAML 2.0 IdP with key ", entityID, e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+ }
+
+ if (idp == null) {
+ throw new NotFoundException("SAML 2.0 IdP '" + entityID + "'");
+ }
+ return idp;
+ }
+
+ @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
+ public SAML2RequestTO createLoginRequest(
+ final String spEntityID, final String idpEntityID) {
+
+ // 1. look for IdP
+ SAML2IdPEntity idp = StringUtils.isBlank(idpEntityID) ? cache.getFirst() : cache.get(idpEntityID);
+ if (idp == null) {
+ if (StringUtils.isBlank(idpEntityID)) {
+ List<SAML2IdP> all = saml2IdPDAO.findAll();
+ if (!all.isEmpty()) {
+ idp = getIdP(all.get(0).getKey());
+ }
+ } else {
+ idp = getIdP(idpEntityID);
+ }
+ }
+ if (idp == null) {
+ throw new NotFoundException(StringUtils.isBlank(idpEntityID)
+ ? "Any SAML 2.0 IdP"
+ : "SAML 2.0 IdP '" + idpEntityID + "'");
+ }
+
+ // 2. create AuthnRequest
+ Issuer issuer = new IssuerBuilder().buildObject();
+ issuer.setValue(spEntityID);
+
+ NameIDPolicy nameIDPolicy = new NameIDPolicyBuilder().buildObject();
+ if (idp.supportsNameIDFormat(NameIDType.TRANSIENT)) {
+ nameIDPolicy.setFormat(NameIDType.TRANSIENT);
+ } else if (idp.supportsNameIDFormat(NameIDType.PERSISTENT)) {
+ nameIDPolicy.setFormat(NameIDType.PERSISTENT);
+ } else {
+ throw new IllegalArgumentException("Could not find supported NameIDFormat for IdP " + idpEntityID);
+ }
+ nameIDPolicy.setAllowCreate(true);
+ nameIDPolicy.setSPNameQualifier(spEntityID);
+
+ AuthnContextClassRef authnContextClassRef = new AuthnContextClassRefBuilder().buildObject();
+ authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
+ RequestedAuthnContext requestedAuthnContext = new RequestedAuthnContextBuilder().buildObject();
+ requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
+ requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
+
+ AuthnRequest authnRequest = new AuthnRequestBuilder().buildObject();
+ authnRequest.setID("_" + UUID_GENERATOR.generate().toString());
+ authnRequest.setAssertionConsumerServiceURL(spEntityID + "saml2sp/assertion-consumer");
+ authnRequest.setForceAuthn(false);
+ authnRequest.setIsPassive(false);
+ authnRequest.setVersion(SAMLVersion.VERSION_20);
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
+ authnRequest.setIssueInstant(new DateTime());
+ authnRequest.setIssuer(issuer);
+ authnRequest.setNameIDPolicy(nameIDPolicy);
+ authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+ authnRequest.setDestination(idp.getSSOLocation(SAMLConstants.SAML2_POST_BINDING_URI).getLocation());
+
+ SAML2RequestTO requestTO = new SAML2RequestTO();
+ requestTO.setIdpServiceAddress(authnRequest.getDestination());
+ try {
+ // 3. sign and encode AuthnRequest
+ requestTO.setContent(saml2Signer.signAndEncode(authnRequest, idp.isUseDeflateEncoding()));
+
+ // 4. generate relay state as JWT
+ Map<String, Object> claims = new HashMap<>();
+ claims.put(JWT_CLAIM_IDP_DEFLATE, idp.isUseDeflateEncoding());
+ Triple<String, String, Date> relayState =
+ accessTokenDataBinder.generateJWT(authnRequest.getID(), JWT_RELAY_STATE_DURATION, claims);
+ requestTO.setRelayState(relayState.getMiddle());
+ } catch (Exception e) {
+ LOG.error("While generating AuthnRequest", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ return requestTO;
+ }
+
+ private List<String> findMatchingUser(final String keyValue, final MappingItemTO connObjectKeyItem) {
+ List<String> result = new ArrayList<>();
+
+ String transformed = keyValue;
+ for (MappingItemTransformer transformer : MappingUtils.getMappingItemTransformers(connObjectKeyItem)) {
+ List<Object> output = transformer.beforePull(
+ null,
+ null,
+ Collections.<Object>singletonList(transformed));
+ if (output != null && !output.isEmpty()) {
+ transformed = output.get(0).toString();
+ }
+ }
+
+ IntAttrName intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName(), AnyTypeKind.USER);
+
+ if (intAttrName.getField() != null) {
+ switch (intAttrName.getField()) {
+ case "key":
+ User byKey = userDAO.find(transformed);
+ if (byKey != null) {
+ result.add(byKey.getKey());
+ }
+ break;
+
+ case "username":
+ User byUsername = userDAO.findByUsername(transformed);
+ if (byUsername != null) {
+ result.add(byUsername.getKey());
+ }
+ break;
+
+ default:
+ }
+ } else if (intAttrName.getSchemaType() != null) {
+ switch (intAttrName.getSchemaType()) {
+ case PLAIN:
+ PlainAttrValue value = entityFactory.newEntity(UPlainAttrValue.class);
+
+ PlainSchema schema = plainSchemaDAO.find(intAttrName.getSchemaName());
+ if (schema == null) {
+ value.setStringValue(transformed);
+ } else {
+ try {
+ value.parseValue(schema, transformed);
+ } catch (ParsingValidationException e) {
+ LOG.error("While parsing provided key value {}", transformed, e);
+ value.setStringValue(transformed);
+ }
+ }
+
+ CollectionUtils.collect(userDAO.findByAttrValue(intAttrName.getSchemaName(), value),
+ EntityUtils.keyTransformer(), result);
+ break;
+
+ case DERIVED:
+ CollectionUtils.collect(userDAO.findByDerAttrValue(intAttrName.getSchemaName(), transformed),
+ EntityUtils.keyTransformer(), result);
+ break;
+
+ default:
+ }
+ }
+
+ return result;
+ }
+
+ private Pair<String, String> extract(final InputStream response) throws IOException {
+ String strForm = IOUtils.toString(response);
+ MultivaluedMap<String, String> params = JAXRSUtils.getStructuredParams(strForm, "&", false, false);
+
+ String samlResponse = URLDecoder.decode(
+ params.getFirst(SSOConstants.SAML_RESPONSE), StandardCharsets.UTF_8.name());
+ LOG.debug("Received SAML Response: {}", samlResponse);
+
+ String relayState = params.getFirst(SSOConstants.RELAY_STATE);
+ LOG.debug("Received Relay State: {}", relayState);
+
+ return Pair.of(samlResponse, relayState);
+ }
+
+ @PreAuthorize("hasRole('" + StandardEntitlement.ANONYMOUS + "')")
+ public SAML2LoginResponseTO validateLoginResponse(final InputStream response) {
+ // 1. extract raw SAML response and relay state
+ Pair<String, String> extracted;
+ try {
+ extracted = extract(response);
+ } catch (Exception e) {
+ LOG.error("While reading AuthnResponse", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ // 2. first checks for the provided relay state
+ JwsJwtCompactConsumer relayState = new JwsJwtCompactConsumer(extracted.getRight());
+ if (!relayState.verifySignatureWith(jwsSignatureCerifier)) {
+ throw new IllegalArgumentException("Invalid signature found in Relay State");
+ }
+ Boolean useDeflateEncoding = Boolean.valueOf(
+ relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
+
+ // 3. parse the provided SAML response
+ Response samlResponse;
+ try {
+ XMLObject responseObject = saml2rw.read(true, useDeflateEncoding, extracted.getLeft());
+ if (!(responseObject instanceof Response)) {
+ throw new IllegalArgumentException("Expected " + Response.class.getName()
+ + ", got " + responseObject.getClass().getName());
+ }
+ samlResponse = (Response) responseObject;
+ } catch (Exception e) {
+ LOG.error("While parsing AuthnResponse", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ // 4. further checks:
+ // 4a. the SAML Reponse's InResponseTo
+ if (!relayState.getJwtClaims().getSubject().equals(samlResponse.getInResponseTo())) {
+ throw new IllegalArgumentException("Unmatching request ID: " + samlResponse.getInResponseTo());
+ }
+ // 4b. the SAML Response status
+ if (!StatusCode.SUCCESS.equals(samlResponse.getStatus().getStatusCode().getValue())) {
+ throw new BadCredentialsException("The SAML IdP replied with "
+ + samlResponse.getStatus().getStatusCode().getValue());
+ }
+
+ // 5. validate the SAML response and, if needed, decrypt the provided assertion(s)
+ SAML2IdPEntity idp = getIdP(samlResponse.getIssuer().getValue());
+ if (idp.getConnObjectKeyItem() == null) {
+ throw new IllegalArgumentException("No mapping provided for SAML 2.0 IdP '" + idp.getId() + "'");
+ }
+ try {
+ saml2rw.validate(samlResponse, idp.getTrustStore());
+ } catch (Exception e) {
+ LOG.error("While validating AuthnResponse", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ // 6. prepare the result: find matching user (if any) and return the received attributes
+ SAML2LoginResponseTO responseTO = new SAML2LoginResponseTO();
+
+ NameID nameID = null;
+ String keyValue = null;
+ for (Assertion assertion : samlResponse.getAssertions()) {
+ nameID = assertion.getSubject().getNameID();
+ if (StringUtils.isNotBlank(nameID.getValue())
+ && idp.getConnObjectKeyItem().getExtAttrName().equals("NameID")) {
+
+ keyValue = nameID.getValue();
+ }
+
+ if (assertion.getConditions().getNotOnOrAfter() != null) {
+ responseTO.setNotOnOrAfter(assertion.getConditions().getNotOnOrAfter().toDate());
+ }
+ for (AuthnStatement authnStmt : assertion.getAuthnStatements()) {
+ responseTO.setSessionIndex(authnStmt.getSessionIndex());
+
+ responseTO.setAuthInstant(authnStmt.getAuthnInstant().toDate());
+ if (authnStmt.getSessionNotOnOrAfter() != null) {
+ responseTO.setNotOnOrAfter(authnStmt.getSessionNotOnOrAfter().toDate());
+ }
+ }
+
+ for (AttributeStatement attrStmt : assertion.getAttributeStatements()) {
+ for (Attribute attr : attrStmt.getAttributes()) {
+ if (!attr.getAttributeValues().isEmpty()) {
+ String attrName = attr.getFriendlyName() == null ? attr.getName() : attr.getFriendlyName();
+ if (attrName.equals(idp.getConnObjectKeyItem().getExtAttrName())
+ && attr.getAttributeValues().get(0) instanceof XSString) {
+
+ keyValue = ((XSString) attr.getAttributeValues().get(0)).getValue();
+ }
+
+ AttrTO attrTO = new AttrTO();
+ attrTO.setSchema(attrName);
+ for (XMLObject value : attr.getAttributeValues()) {
+ if (value.getDOM() != null) {
+ attrTO.getValues().add(value.getDOM().getTextContent());
+ }
+ }
+ responseTO.getAttrs().add(attrTO);
+ }
+ }
+ }
+ }
+ if (nameID == null) {
+ throw new IllegalArgumentException("NameID not found");
+ }
+
+ List<String> matchingUsers = keyValue == null
+ ? Collections.<String>emptyList()
+ : findMatchingUser(keyValue, idp.getConnObjectKeyItem());
+ LOG.debug("Found {} matching users for NameID {}", matchingUsers.size(), nameID.getValue());
+
+ if (matchingUsers.isEmpty()) {
+ throw new NotFoundException("User matching the provided NameID value " + nameID.getValue());
+ } else if (matchingUsers.size() > 1) {
+ throw new IllegalArgumentException("Several users match the provided NameID value " + nameID.getValue());
+ }
+ responseTO.setUsername(userDAO.find(matchingUsers.get(0)).getUsername());
+
+ responseTO.setNameID(nameID.getValue());
+ // 7. generate JWT for further access
+ Map<String, Object> claims = new HashMap<>();
+ claims.put(JWT_CLAIM_IDP_ENTITYID, idp.getId());
+ claims.put(JWT_CLAIM_NAMEID_FORMAT, nameID.getFormat());
+ claims.put(JWT_CLAIM_NAMEID_VALUE, nameID.getValue());
+ claims.put(JWT_CLAIM_SESSIONINDEX, responseTO.getSessionIndex());
+ responseTO.setAccessToken(accessTokenDataBinder.create(responseTO.getUsername(), claims, true));
+
+ return responseTO;
+ }
+
+ @PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
+ public SAML2RequestTO createLogoutRequest(final String accessToken, final String spEntityID) {
+ // 1. fetch the current JWT used for Syncope authentication
+ JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
+ if (!consumer.verifySignatureWith(jwsSignatureCerifier)) {
+ throw new IllegalArgumentException("Invalid signature found in Access Token");
+ }
+
+ // 2. look for IdP
+ String idpEntityID = (String) consumer.getJwtClaims().getClaim(JWT_CLAIM_IDP_ENTITYID);
+ SAML2IdPEntity idp = cache.get(idpEntityID);
+ if (idp == null) {
+ throw new NotFoundException("SAML 2.0 IdP '" + idpEntityID + "'");
+ }
+ if (idp.getSLOLocation(SAMLConstants.SAML2_POST_BINDING_URI) == null) {
+ throw new IllegalArgumentException("No SingleLogoutService available for " + idp.getId());
+ }
+
+ // 3. create LogoutRequest
+ LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
+ logoutRequest.setID("_" + UUID_GENERATOR.generate().toString());
+ logoutRequest.setDestination(idp.getSLOLocation(SAMLConstants.SAML2_POST_BINDING_URI).getLocation());
+
+ DateTime now = new DateTime();
+ logoutRequest.setIssueInstant(now);
+ logoutRequest.setNotOnOrAfter(now.plusMinutes(5));
+
+ Issuer issuer = new IssuerBuilder().buildObject();
+ issuer.setValue(spEntityID);
+ logoutRequest.setIssuer(issuer);
+
+ NameID nameID = new NameIDBuilder().buildObject();
+ nameID.setFormat((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_FORMAT));
+ nameID.setValue((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_NAMEID_VALUE));
+ logoutRequest.setNameID(nameID);
+
+ SessionIndex sessionIndex = new SessionIndexBuilder().buildObject();
+ sessionIndex.setSessionIndex((String) consumer.getJwtClaims().getClaim(JWT_CLAIM_SESSIONINDEX));
+ logoutRequest.getSessionIndexes().add(sessionIndex);
+
+ SAML2RequestTO requestTO = new SAML2RequestTO();
+ requestTO.setIdpServiceAddress(logoutRequest.getDestination());
+ try {
+ // 3. sign and encode LogoutRequest
+ requestTO.setContent(saml2Signer.signAndEncode(logoutRequest, idp.isUseDeflateEncoding()));
+
+ // 4. generate relay state as JWT
+ Map<String, Object> claims = new HashMap<>();
+ claims.put(JWT_CLAIM_IDP_DEFLATE, idp.isUseDeflateEncoding());
+ Triple<String, String, Date> relayState =
+ accessTokenDataBinder.generateJWT(logoutRequest.getID(), JWT_RELAY_STATE_DURATION, claims);
+ requestTO.setRelayState(relayState.getMiddle());
+ } catch (Exception e) {
+ LOG.error("While generating LogoutRequest", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ return requestTO;
+ }
+
+ @PreAuthorize("isAuthenticated() and not(hasRole('" + StandardEntitlement.ANONYMOUS + "'))")
+ public void validateLogoutResponse(final String accessToken, final InputStream response) {
+ // 1. fetch the current JWT used for Syncope authentication
+ JwsJwtCompactConsumer consumer = new JwsJwtCompactConsumer(accessToken);
+ if (!consumer.verifySignatureWith(jwsSignatureCerifier)) {
+ throw new IllegalArgumentException("Invalid signature found in Access Token");
+ }
+
+ // 2. extract raw SAML response and relay state
+ Pair<String, String> extracted;
+ try {
+ extracted = extract(response);
+ } catch (Exception e) {
+ LOG.error("While reading LogoutResponse", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ JwsJwtCompactConsumer relayState = null;
+ Boolean useDeflateEncoding = false;
+ if (StringUtils.isNotBlank(extracted.getRight())) {
+ // first checks for the provided relay state, if available
+ relayState = new JwsJwtCompactConsumer(extracted.getRight());
+ if (!relayState.verifySignatureWith(jwsSignatureCerifier)) {
+ throw new IllegalArgumentException("Invalid signature found in Relay State");
+ }
+ useDeflateEncoding = Boolean.valueOf(
+ relayState.getJwtClaims().getClaim(JWT_CLAIM_IDP_DEFLATE).toString());
+ }
+
+ // 3. parse the provided SAML response
+ LogoutResponse logoutResponse;
+ try {
+ XMLObject responseObject = saml2rw.read(true, useDeflateEncoding, extracted.getLeft());
+ if (!(responseObject instanceof LogoutResponse)) {
+ throw new IllegalArgumentException("Expected " + LogoutResponse.class.getName()
+ + ", got " + responseObject.getClass().getName());
+ }
+ logoutResponse = (LogoutResponse) responseObject;
+ } catch (Exception e) {
+ LOG.error("While parsing LogoutResponse", e);
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ sce.getElements().add(e.getMessage());
+ throw sce;
+ }
+
+ // 4. if relay state was available, check the SAML Reponse's InResponseTo
+ if (relayState != null && !relayState.getJwtClaims().getSubject().equals(logoutResponse.getInResponseTo())) {
+ throw new IllegalArgumentException("Unmatching request ID: " + logoutResponse.getInResponseTo());
+ }
+
+ // 5. finally check for the logout status
+ if (StatusCode.SUCCESS.equals(logoutResponse.getStatus().getStatusCode().getValue())) {
+ accessTokenDAO.delete(consumer.getJwtClaims().getTokenId());
+ } else {
+ SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+ if (logoutResponse.getStatus().getStatusMessage() == null) {
+ sce.getElements().add(logoutResponse.getStatus().getStatusCode().getValue());
+ } else {
+ sce.getElements().add(logoutResponse.getStatus().getStatusMessage().getMessage());
+ }
+ throw sce;
+ }
+ }
+
+ @Override
+ protected AbstractBaseBean resolveReference(
+ final Method method, final Object... args) throws UnresolvedReferenceException {
+
+ throw new UnresolvedReferenceException();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java
new file mode 100644
index 0000000..dee85ef
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.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.core.logic.init;
+
+import java.io.File;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.core.persistence.api.SyncopeLoader;
+import org.apache.syncope.core.provisioning.api.EntitlementsHolder;
+import org.apache.syncope.common.lib.types.SAML2SPEntitlement;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.apache.syncope.core.spring.ResourceWithFallbackLoader;
+import org.opensaml.core.criterion.EntityIdCriterion;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.impl.KeyStoreCredentialResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SAML2SPLoader implements SyncopeLoader {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SAML2SPLoader.class);
+
+ private static final String SAML2SP_LOGIC_PROPERTIES = "saml2sp-logic.properties";
+
+ private static <T> T assertNotNull(final T argument, final String name) {
+ if (argument == null) {
+ throw new IllegalArgumentException("Argument '" + name + "' may not be null.");
+ }
+ return argument;
+ }
+
+ private KeyStore keystore;
+
+ private String keyPass;
+
+ private Credential credential;
+
+ @Override
+ public Integer getPriority() {
+ return 1000;
+ }
+
+ @Override
+ public void load() {
+ EntitlementsHolder.getInstance().init(SAML2SPEntitlement.values());
+
+ String confDirectory = null;
+
+ Properties props = new Properties();
+ try (InputStream is = getClass().getResourceAsStream("/" + SAML2SP_LOGIC_PROPERTIES)) {
+ props.load(is);
+ confDirectory = props.getProperty("conf.directory");
+
+ File confDir = new File(confDirectory);
+ if (confDir.exists() && confDir.canRead() && confDir.isDirectory()) {
+ File confDirProps = FileUtils.getFile(confDir, SAML2SP_LOGIC_PROPERTIES);
+ if (confDirProps.exists() && confDirProps.canRead() && confDirProps.isFile()) {
+ props.clear();
+ props.load(FileUtils.openInputStream(confDirProps));
+ confDirectory = props.getProperty("conf.directory");
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Could not read " + SAML2SP_LOGIC_PROPERTIES, e);
+ }
+
+ assertNotNull(confDirectory, "<conf.directory>");
+
+ String name = props.getProperty("keystore.name");
+ assertNotNull(name, "<keystore.name>");
+ String type = props.getProperty("keystore.type");
+ assertNotNull(type, "<keystore.type>");
+ String storePass = props.getProperty("keystore.storepass");
+ assertNotNull(storePass, "<keystore.storepass>");
+ keyPass = props.getProperty("keystore.keypass");
+ assertNotNull(keyPass, "<keystore.keypass>");
+ String certAlias = props.getProperty("sp.cert.alias");
+ assertNotNull(certAlias, "<sp.cert.alias>");
+
+ LOG.debug("Attempting to load the provided keystore...");
+ try {
+ ResourceWithFallbackLoader loader = new ResourceWithFallbackLoader();
+ loader.setResourceLoader(ApplicationContextProvider.getApplicationContext());
+ loader.setPrimary(StringUtils.appendIfMissing("file:" + confDirectory, "/") + name);
+ loader.setFallback("classpath:" + name);
+
+ keystore = KeyStore.getInstance(type);
+ try (InputStream inputStream = loader.getResource().getInputStream()) {
+ keystore.load(inputStream, storePass.toCharArray());
+ LOG.debug("Keystore loaded");
+ }
+
+ Map<String, String> passwordMap = new HashMap<>();
+ passwordMap.put(certAlias, keyPass);
+ KeyStoreCredentialResolver resolver = new KeyStoreCredentialResolver(keystore, passwordMap);
+
+ this.credential = resolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(certAlias)));
+ LOG.debug("SAML 2.0 Service Provider certificate loaded");
+ } catch (Exception e) {
+ throw new RuntimeException("Could not initialize the SAML 2.0 Service Provider certificate", e);
+ }
+ }
+
+ public KeyStore getKeyStore() {
+ return keystore;
+ }
+
+ public String getKeyPass() {
+ return keyPass;
+ }
+
+ public Credential getCredential() {
+ return credential;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCache.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCache.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCache.java
new file mode 100644
index 0000000..21e185d
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCache.java
@@ -0,0 +1,90 @@
+/*
+ * 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.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import net.shibboleth.utilities.java.support.xml.XMLParserException;
+import org.apache.syncope.common.lib.to.MappingItemTO;
+import org.apache.syncope.core.logic.init.SAML2SPLoader;
+import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
+import org.apache.syncope.core.provisioning.api.data.SAML2IdPDataBinder;
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.saml.OpenSAMLUtil;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.w3c.dom.Element;
+
+/**
+ * Basic in-memory cache for available {@link SAML2IdPEntity} identity providers.
+ */
+@Component
+public class SAML2IdPCache {
+
+ private final Map<String, SAML2IdPEntity> cache =
+ Collections.synchronizedMap(new HashMap<String, SAML2IdPEntity>());
+
+ @Autowired
+ private SAML2SPLoader loader;
+
+ @Autowired
+ private SAML2IdPDataBinder binder;
+
+ public SAML2IdPEntity get(final String entityID) {
+ return cache.get(entityID);
+ }
+
+ public SAML2IdPEntity getFirst() {
+ return cache.isEmpty() ? null : cache.entrySet().iterator().next().getValue();
+ }
+
+ public SAML2IdPEntity put(
+ final EntityDescriptor entityDescriptor,
+ final MappingItemTO connObjectKeyItem,
+ final boolean useDeflateEncoding)
+ throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException {
+
+ return cache.put(entityDescriptor.getEntityID(),
+ new SAML2IdPEntity(entityDescriptor, connObjectKeyItem, useDeflateEncoding, loader.getKeyPass()));
+ }
+
+ @Transactional(readOnly = true)
+ public SAML2IdPEntity put(final SAML2IdP idp)
+ throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, WSSecurityException,
+ XMLParserException {
+
+ Element element = OpenSAMLUtil.getParserPool().parse(
+ new InputStreamReader(new ByteArrayInputStream(idp.getMetadata()))).getDocumentElement();
+ EntityDescriptor entityDescriptor = (EntityDescriptor) OpenSAMLUtil.fromDom(element);
+ return put(entityDescriptor, binder.getIdPTO(idp).getConnObjectKeyItem(), idp.isUseDeflateEncoding());
+ }
+
+ public SAML2IdPEntity remove(final String entityID) {
+ return cache.remove(entityID);
+ }
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCallbackHandler.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCallbackHandler.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCallbackHandler.java
new file mode 100644
index 0000000..17cf6f0
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPCallbackHandler.java
@@ -0,0 +1,45 @@
+/*
+ * 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.io.IOException;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.wss4j.common.ext.WSPasswordCallback;
+
+public class SAML2IdPCallbackHandler implements CallbackHandler {
+
+ private final String keyPass;
+
+ public SAML2IdPCallbackHandler(final String keyPass) {
+ this.keyPass = keyPass;
+ }
+
+ @Override
+ public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof WSPasswordCallback) {
+ WSPasswordCallback wspc = (WSPasswordCallback) callback;
+ wspc.setPassword(keyPass);
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPEntity.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPEntity.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPEntity.java
new file mode 100644
index 0000000..35eacaf
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2IdPEntity.java
@@ -0,0 +1,155 @@
+/*
+ * 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.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.syncope.common.lib.to.MappingItemTO;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.metadata.Endpoint;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.KeyDescriptor;
+import org.opensaml.saml.saml2.metadata.NameIDFormat;
+import org.opensaml.saml.saml2.metadata.SingleLogoutService;
+import org.opensaml.saml.saml2.metadata.SingleSignOnService;
+import org.opensaml.xmlsec.signature.X509Data;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SAML2IdPEntity {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SAML2IdPEntity.class);
+
+ private final String id;
+
+ private boolean useDeflateEncoding;
+
+ private MappingItemTO connObjectKeyItem;
+
+ private final Map<String, Endpoint> ssoBindings = new HashMap<>();
+
+ private final Map<String, SingleLogoutService> sloBindings = new HashMap<>();
+
+ private final List<String> nameIDFormats = new ArrayList<>();
+
+ private final KeyStore trustStore;
+
+ public SAML2IdPEntity(
+ final EntityDescriptor entityDescriptor,
+ final MappingItemTO connObjectKeyItem,
+ final boolean useDeflateEncoding,
+ final String keyPass)
+ throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException {
+
+ this.id = entityDescriptor.getEntityID();
+ this.connObjectKeyItem = connObjectKeyItem;
+ this.useDeflateEncoding = useDeflateEncoding;
+
+ IDPSSODescriptor idpdescriptor = entityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
+
+ for (SingleSignOnService sso : idpdescriptor.getSingleSignOnServices()) {
+ LOG.debug("[{}] Add SSO binding {}({})", id, sso.getBinding(), sso.getLocation());
+ this.ssoBindings.put(sso.getBinding(), sso);
+ }
+
+ for (SingleLogoutService slo : idpdescriptor.getSingleLogoutServices()) {
+ LOG.debug("[{}] Add SLO binding '{}'\n\tLocation: '{}'\n\tResponse Location: '{}'",
+ id, slo.getBinding(), slo.getLocation(), slo.getResponseLocation());
+ this.sloBindings.put(slo.getBinding(), slo);
+ }
+
+ for (NameIDFormat nameIDFormat : idpdescriptor.getNameIDFormats()) {
+ LOG.debug("[{}] Add NameIDFormat '{}'", id, nameIDFormat.getFormat());
+ nameIDFormats.add(nameIDFormat.getFormat());
+ }
+
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+ List<X509Certificate> chain = new ArrayList<>();
+ for (KeyDescriptor key : idpdescriptor.getKeyDescriptors()) {
+ for (X509Data x509Data : key.getKeyInfo().getX509Datas()) {
+ for (org.opensaml.xmlsec.signature.X509Certificate cert : x509Data.getX509Certificates()) {
+ try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decodeBase64(cert.getValue()))) {
+ chain.add(X509Certificate.class.cast(cf.generateCertificate(bais)));
+ }
+ }
+ }
+ }
+
+ this.trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ this.trustStore.load(null, keyPass.toCharArray());
+ if (!chain.isEmpty()) {
+ for (X509Certificate cert : chain) {
+ LOG.debug("[{}] Add X.509 certificate {}", id, cert.getSubjectX500Principal().getName());
+ this.trustStore.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert);
+ }
+ LOG.debug("[{}] Set default X.509 certificate {}", id, chain.get(0).getSubjectX500Principal().getName());
+ this.trustStore.setCertificateEntry(id, chain.get(0));
+ }
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean isUseDeflateEncoding() {
+ return useDeflateEncoding;
+ }
+
+ public void setUseDeflateEncoding(final boolean useDeflateEncoding) {
+ this.useDeflateEncoding = useDeflateEncoding;
+ }
+
+ public MappingItemTO getConnObjectKeyItem() {
+ return connObjectKeyItem;
+ }
+
+ public void setConnObjectKeyItem(final MappingItemTO connObjectKeyItem) {
+ this.connObjectKeyItem = connObjectKeyItem;
+ }
+
+ public Endpoint getSSOLocation(final String binding) {
+ return ssoBindings.get(binding);
+ }
+
+ public Endpoint getSLOLocation(final String binding) {
+ return sloBindings.get(binding);
+ }
+
+ public boolean supportsNameIDFormat(final String nameIDFormat) {
+ return nameIDFormats.contains(nameIDFormat);
+ }
+
+ public KeyStore getTrustStore() {
+ return trustStore;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java
new file mode 100644
index 0000000..23b3a38
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2ReaderWriter.java
@@ -0,0 +1,145 @@
+/*
+ * 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.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.zip.DataFormatException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder;
+import org.apache.cxf.rs.security.saml.sso.SAMLProtocolResponseValidator;
+import org.apache.cxf.staxutils.StaxUtils;
+import org.apache.syncope.core.logic.init.SAML2SPLoader;
+import org.apache.wss4j.common.crypto.Merlin;
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.saml.OpenSAMLUtil;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.saml.saml2.core.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.w3c.dom.Document;
+
+@Component
+public class SAML2ReaderWriter implements InitializingBean {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SAML2ReaderWriter.class);
+
+ private static final TransformerFactory TRANSFORMER_FACTORY = TransformerFactory.newInstance();
+
+ static {
+ OpenSAMLUtil.initSamlEngine(false);
+ }
+
+ @Autowired
+ private SAML2SPLoader loader;
+
+ private SAMLProtocolResponseValidator protocolValidator;
+
+ private SAML2IdPCallbackHandler callbackHandler;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ protocolValidator = new SAMLProtocolResponseValidator();
+ protocolValidator.setKeyInfoMustBeAvailable(true);
+
+ callbackHandler = new SAML2IdPCallbackHandler(loader.getKeyPass());
+ }
+
+ public void write(final Writer writer, final XMLObject object, final boolean signObject)
+ throws TransformerConfigurationException, WSSecurityException, TransformerException {
+
+ Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
+ StreamResult streamResult = new StreamResult(writer);
+ DOMSource source = new DOMSource(OpenSAMLUtil.toDom(object, null, signObject));
+ transformer.transform(source, streamResult);
+ }
+
+ public XMLObject read(final boolean postBinding, final boolean useDeflateEncoding, final String response)
+ throws DataFormatException, UnsupportedEncodingException, XMLStreamException, WSSecurityException {
+
+ String decodedResponse = response;
+ // URL Decoding only applies for the redirect binding
+ if (!postBinding) {
+ decodedResponse = URLDecoder.decode(response, StandardCharsets.UTF_8.name());
+ }
+
+ InputStream tokenStream;
+ byte[] deflatedToken = Base64.decodeBase64(decodedResponse);
+ tokenStream = !postBinding && useDeflateEncoding
+ ? new DeflateEncoderDecoder().inflateToken(deflatedToken)
+ : new ByteArrayInputStream(deflatedToken);
+
+ // parse the provided SAML response
+ Document responseDoc = StaxUtils.read(new InputStreamReader(tokenStream, StandardCharsets.UTF_8));
+ XMLObject responseObject = OpenSAMLUtil.fromDom(responseDoc.getDocumentElement());
+
+ if (LOG.isDebugEnabled()) {
+ try {
+ StringWriter writer = new StringWriter();
+ write(writer, responseObject, false);
+ writer.close();
+
+ LOG.debug("Parsed SAML response: {}", writer.toString());
+ } catch (Exception e) {
+ LOG.error("Could not log the received SAML response", e);
+ }
+ }
+
+ return responseObject;
+ }
+
+ public void validate(final Response samlResponse, final KeyStore idpTrustStore) throws WSSecurityException {
+ // validate the SAML response and, if needed, decrypt the provided assertion(s)
+ Merlin crypto = new Merlin();
+ crypto.setKeyStore(loader.getKeyStore());
+ crypto.setTrustStore(idpTrustStore);
+
+ protocolValidator.validateSamlResponse(samlResponse, crypto, callbackHandler);
+
+ if (LOG.isDebugEnabled()) {
+ try {
+ StringWriter writer = new StringWriter();
+ write(writer, samlResponse, false);
+ writer.close();
+
+ LOG.debug("SAML response with decrypted assertions: {}", writer.toString());
+ } catch (Exception e) {
+ LOG.error("Could not log the SAML response with decrypted assertions", e);
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2Signer.java
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2Signer.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2Signer.java
new file mode 100644
index 0000000..9a03627
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2Signer.java
@@ -0,0 +1,104 @@
+/*
+ * 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.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import javax.xml.transform.TransformerException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.cxf.rs.security.saml.DeflateEncoderDecoder;
+import org.apache.syncope.core.logic.init.SAML2SPLoader;
+import org.apache.wss4j.common.ext.WSSecurityException;
+import org.apache.wss4j.common.saml.OpenSAMLUtil;
+import org.opensaml.saml.common.SignableSAMLObject;
+import org.opensaml.saml.saml2.core.RequestAbstractType;
+import org.opensaml.security.SecurityException;
+import org.opensaml.xmlsec.keyinfo.KeyInfoGenerator;
+import org.opensaml.xmlsec.keyinfo.impl.X509KeyInfoGeneratorFactory;
+import org.opensaml.xmlsec.signature.Signature;
+import org.opensaml.xmlsec.signature.support.SignatureConstants;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SAML2Signer implements InitializingBean {
+
+ static {
+ OpenSAMLUtil.initSamlEngine(false);
+ }
+
+ @Autowired
+ private SAML2SPLoader loader;
+
+ @Autowired
+ private SAML2ReaderWriter saml2rw;
+
+ private KeyInfoGenerator keyInfoGenerator;
+
+ private String signatureAlgorithm;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
+ keyInfoGeneratorFactory.setEmitEntityCertificate(true);
+ keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
+
+ signatureAlgorithm = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1;
+ String pubKeyAlgo = loader.getCredential().getPublicKey().getAlgorithm();
+ if (pubKeyAlgo.equalsIgnoreCase("DSA")) {
+ signatureAlgorithm = SignatureConstants.ALGO_ID_SIGNATURE_DSA_SHA1;
+ }
+ }
+
+ public String signAndEncode(final RequestAbstractType request, final boolean useDeflateEncoding)
+ throws SecurityException, WSSecurityException, TransformerException, IOException {
+
+ // 1. sign request
+ Signature signature = OpenSAMLUtil.buildSignature();
+ signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
+ signature.setSignatureAlgorithm(signatureAlgorithm);
+ signature.setSigningCredential(loader.getCredential());
+ signature.setKeyInfo(keyInfoGenerator.generate(loader.getCredential()));
+
+ SignableSAMLObject signableObject = (SignableSAMLObject) request;
+ signableObject.setSignature(signature);
+ signableObject.releaseDOM();
+ signableObject.releaseChildrenDOM(true);
+
+ // 2. serialize and encode request
+ StringWriter writer = new StringWriter();
+ saml2rw.write(writer, request, true);
+ writer.close();
+
+ String requestMessage = writer.toString();
+ byte[] deflatedBytes;
+ // not correct according to the spec but required by some IdPs.
+ if (useDeflateEncoding) {
+ deflatedBytes = new DeflateEncoderDecoder().
+ deflateToken(requestMessage.getBytes(StandardCharsets.UTF_8));
+ } else {
+ deflatedBytes = requestMessage.getBytes(StandardCharsets.UTF_8);
+ }
+
+ return Base64.encodeBase64String(deflatedBytes);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/syncope/blob/ff26658e/ext/saml2sp/logic/src/main/resources/saml2sp-logic.properties
----------------------------------------------------------------------
diff --git a/ext/saml2sp/logic/src/main/resources/saml2sp-logic.properties b/ext/saml2sp/logic/src/main/resources/saml2sp-logic.properties
new file mode 100644
index 0000000..2d7e918
--- /dev/null
+++ b/ext/saml2sp/logic/src/main/resources/saml2sp-logic.properties
@@ -0,0 +1,23 @@
+# 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.
+conf.directory=${conf.directory}
+
+keystore.name=keystore
+keystore.type=jks
+keystore.storepass=changeit
+keystore.keypass=changeit
+sp.cert.alias=sp