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