You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by mm...@apache.org on 2020/05/12 09:31:55 UTC

[syncope] branch master updated: SYNCOPE-1558: restful SAML SP metadata/keystore for WA authN delegation (#181)

This is an automated email from the ASF dual-hosted git repository.

mmoayyed pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 272fcb7  SYNCOPE-1558: restful SAML SP metadata/keystore for WA authN delegation (#181)
272fcb7 is described below

commit 272fcb759421bec3ea467b9fad92ea9df69cbccc
Author: Misagh Moayyed <mm...@gmail.com>
AuthorDate: Tue May 12 14:01:46 2020 +0430

    SYNCOPE-1558: restful SAML SP metadata/keystore for WA authN delegation (#181)
---
 .../common/lib/auth/SAML2IdPAuthModuleConf.java    |  25 ----
 .../syncope/common/lib/to/SAML2SPKeystoreTO.java   | 125 ++++++++++++++++
 .../syncope/common/lib/to/SAML2SPMetadataTO.java   | 126 +++++++++++++++++
 .../syncope/common/lib/types/AMEntitlement.java    |  12 ++
 .../api/service/SAML2SPKeystoreConfService.java    |  64 +++++++++
 .../api/service/SAML2SPMetadataConfService.java    |  64 +++++++++
 .../service/{ => wa}/SAML2IdPMetadataService.java  |   7 +-
 .../SAML2SPKeystoreService.java}                   |  58 ++++----
 .../SAML2SPMetadataService.java}                   |  58 ++++----
 .../syncope/core/logic/SAML2SPKeystoreLogic.java   | 113 +++++++++++++++
 .../syncope/core/logic/SAML2SPMetadataLogic.java   | 113 +++++++++++++++
 .../service/SAML2SPKeystoreConfServiceImpl.java    |  37 +++++
 .../service/SAML2SPMetadataConfServiceImpl.java    |  37 +++++
 .../{ => wa}/SAML2IdPMetadataServiceImpl.java      |   7 +-
 .../SAML2SPKeystoreServiceImpl.java}               |  35 ++---
 .../SAML2SPMetadataServiceImpl.java}               |  35 ++---
 .../api/dao/auth/SAML2SPKeystoreDAO.java           |  32 +++++
 .../api/dao/auth/SAML2SPMetadataDAO.java           |  32 +++++
 .../api/entity/auth/SAML2SPKeystore.java           |  32 +++++
 .../api/entity/auth/SAML2SPMetadata.java           |  32 +++++
 core/persistence-jpa-json/pom.xml                  |  52 ++++---
 .../src/test/resources/domains/MasterContent.xml   |   5 +-
 core/persistence-jpa/pom.xml                       |  10 ++
 .../jpa/dao/auth/JPASAML2SPKeystoreDAO.java        |  62 ++++++++
 .../jpa/dao/auth/JPASAML2SPMetadataDAO.java        |  62 ++++++++
 .../persistence/jpa/entity/JPAEntityFactory.java   |   8 ++
 .../jpa/entity/auth/JPASAML2SPKeystore.java        |  64 +++++++++
 .../jpa/entity/auth/JPASAML2SPMetadata.java        |  64 +++++++++
 .../core/persistence/jpa/inner/AuthModuleTest.java |   2 -
 .../persistence/jpa/inner/SAML2SPKeystoreTest.java | 157 +++++++++++++++++++++
 .../persistence/jpa/inner/SAML2SPMetadataTest.java |  83 +++++++++++
 .../src/test/resources/domains/MasterContent.xml   |   5 +-
 .../src/test/resources/idp-metadata.xml            | 144 +++++++++++++++++++
 .../src/test/resources/sp-metadata.xml             |  43 ++++++
 .../api/data/SAML2SPKeystoreBinder.java            |  32 +++++
 .../api/data/SAML2SPMetadataBinder.java            |  32 +++++
 .../java/data/SAML2SPKeystoreBinderImpl.java       |  71 ++++++++++
 .../java/data/SAML2SPMetadataBinderImpl.java       |  72 ++++++++++
 fit/core-reference/pom.xml                         |  25 ++++
 .../org/apache/syncope/fit/AbstractITCase.java     |  48 ++++++-
 .../apache/syncope/fit/core/AuthModuleITCase.java  |   2 -
 .../syncope/fit/core/SAML2IdPMetadataITCase.java   |   8 +-
 .../syncope/fit/core/SAML2SPKeystoreITCase.java    | 104 ++++++++++++++
 .../syncope/fit/core/SAML2SPMetadataITCase.java    | 104 ++++++++++++++
 .../org/apache/syncope/fit/core/SearchITCase.java  |   1 -
 .../src/main/resources/idp-metadata.xml            | 144 +++++++++++++++++++
 pom.xml                                            |  13 +-
 .../bootstrap/SyncopeWAPropertySourceLocator.java  |  12 +-
 wa/starter/pom.xml                                 |   8 ++
 .../syncope/wa/starter/SyncopeWAApplication.java   |  20 +--
 .../syncope/wa/starter/SyncopeWAConfiguration.java |  61 ++++----
 .../wa/starter/SyncopeWARefreshContextJob.java     |  13 +-
 .../pac4j/saml/SyncopeWASAML2ClientCustomizer.java |  50 +++++++
 .../SyncopeWASAML2ClientKeystoreGenerator.java     | 109 ++++++++++++++
 .../SyncopeWASAML2ClientMetadataGenerator.java     |  77 ++++++++++
 .../pac4j/saml/SyncopeWASAML2MetadataResolver.java |  63 +++++++++
 .../metadata/RestfulSamlIdPMetadataGenerator.java  |   2 +-
 .../metadata/RestfulSamlIdPMetadataLocator.java    |   4 +-
 wa/starter/src/main/resources/wa.properties        |   2 +-
 .../pac4j/saml/BaseSyncopeWASAML2ClientTest.java   | 117 +++++++++++++++
 .../saml/SyncopeWASAML2ClientCustomizerTest.java   |  80 +++++++++++
 .../SyncopeWASAML2ClientKeystoreGeneratorTest.java |  93 ++++++++++++
 .../SyncopeWASAML2ClientMetadataGeneratorTest.java |  90 ++++++++++++
 .../saml/SyncopeWASAML2MetadataResolverTest.java   |  67 +++++++++
 wa/starter/src/test/resources/idp-metadata.xml     | 144 +++++++++++++++++++
 wa/starter/src/test/resources/sp-metadata.xml      |  43 ++++++
 66 files changed, 3305 insertions(+), 211 deletions(-)

diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
index 03b6aed..32079b4 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/auth/SAML2IdPAuthModuleConf.java
@@ -55,10 +55,6 @@ public class SAML2IdPAuthModuleConf extends AbstractAuthModuleConf {
      */
     private String privateKeyPassword;
 
-    /**
-     * Location of the keystore to use and generate the SP keystore.
-     */
-    private String keystorePath;
 
     /**
      * The metadata location of the identity provider that is to handle authentications.
@@ -93,11 +89,6 @@ public class SAML2IdPAuthModuleConf extends AbstractAuthModuleConf {
     private String serviceProviderEntityId;
 
     /**
-     * Location of the SP metadata to use and generate.
-     */
-    private String serviceProviderMetadataPath;
-
-    /**
      * Whether authentication requests should be tagged as forced auth.
      */
     private boolean forceAuth;
@@ -232,14 +223,6 @@ public class SAML2IdPAuthModuleConf extends AbstractAuthModuleConf {
         this.privateKeyPassword = privateKeyPassword;
     }
 
-    public String getKeystorePath() {
-        return keystorePath;
-    }
-
-    public void setKeystorePath(final String keystorePath) {
-        this.keystorePath = keystorePath;
-    }
-
     public String getIdentityProviderMetadataPath() {
         return identityProviderMetadataPath;
     }
@@ -272,14 +255,6 @@ public class SAML2IdPAuthModuleConf extends AbstractAuthModuleConf {
         this.serviceProviderEntityId = serviceProviderEntityId;
     }
 
-    public String getServiceProviderMetadataPath() {
-        return serviceProviderMetadataPath;
-    }
-
-    public void setServiceProviderMetadataPath(final String serviceProviderMetadataPath) {
-        this.serviceProviderMetadataPath = serviceProviderMetadataPath;
-    }
-
     public boolean isForceAuth() {
         return forceAuth;
     }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPKeystoreTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPKeystoreTO.java
new file mode 100644
index 0000000..c3cd496
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPKeystoreTO.java
@@ -0,0 +1,125 @@
+/*
+ * 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 org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+
+import javax.ws.rs.PathParam;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlRootElement(name = "saml2spMetadataKeystore")
+@XmlType
+public class SAML2SPKeystoreTO extends BaseBean implements EntityTO {
+
+    private static final long serialVersionUID = 3211073386484148953L;
+
+    private String key;
+
+    private String keystore;
+
+    private String owner;
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    @PathParam("key")
+    @Override
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public String getKeystore() {
+        return keystore;
+    }
+
+    public void setKeystore(final String keystore) {
+        this.keystore = keystore;
+    }
+
+    public String getOwner() {
+        return owner;
+    }
+
+    public void setOwner(final String owner) {
+        this.owner = owner;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+            append(key).
+            append(keystore).
+            append(owner).
+            build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SAML2SPKeystoreTO other = (SAML2SPKeystoreTO) obj;
+        return new EqualsBuilder().
+            append(key, other.key).
+            append(keystore, other.keystore).
+            append(owner, other.owner).
+            build();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+            .appendSuper(super.toString())
+            .append("key", key)
+            .append("owner", owner)
+            .toString();
+    }
+
+    public static class Builder {
+
+        private final SAML2SPKeystoreTO instance = new SAML2SPKeystoreTO();
+
+        public Builder keystore(final String keystore) {
+            instance.setKeystore(keystore);
+            return this;
+        }
+
+        public Builder owner(final String owner) {
+            instance.setOwner(owner);
+            return this;
+        }
+
+        public SAML2SPKeystoreTO build() {
+            return instance;
+        }
+    }
+
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPMetadataTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPMetadataTO.java
new file mode 100644
index 0000000..104b90f
--- /dev/null
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/SAML2SPMetadataTO.java
@@ -0,0 +1,126 @@
+/*
+ * 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 org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.syncope.common.lib.BaseBean;
+
+import javax.ws.rs.PathParam;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlRootElement(name = "saml2spMetadata")
+@XmlType
+public class SAML2SPMetadataTO extends BaseBean implements EntityTO {
+
+    private static final long serialVersionUID = 6215073386484048953L;
+
+    private String key;
+
+    private String metadata;
+
+    private String owner;
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    @PathParam("key")
+    @Override
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public String getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(final String metadata) {
+        this.metadata = metadata;
+    }
+
+    public String getOwner() {
+        return owner;
+    }
+
+    public void setOwner(final String owner) {
+        this.owner = owner;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+            append(key).
+            append(metadata).
+            append(owner).
+            build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SAML2SPMetadataTO other = (SAML2SPMetadataTO) obj;
+        return new EqualsBuilder().
+            append(key, other.key).
+            append(metadata, other.metadata).
+            append(owner, other.owner).
+            build();
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this)
+            .appendSuper(super.toString())
+            .append("key", key)
+            .append("owner", owner)
+            .toString();
+    }
+
+
+    public static class Builder {
+
+        private final SAML2SPMetadataTO instance = new SAML2SPMetadataTO();
+
+        public Builder metadata(final String metadata) {
+            instance.setMetadata(metadata);
+            return this;
+        }
+
+        public Builder owner(final String owner) {
+            instance.setOwner(owner);
+            return this;
+        }
+
+        public SAML2SPMetadataTO build() {
+            return instance;
+        }
+    }
+
+}
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
index eefe798..6f28b1f 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
@@ -60,6 +60,18 @@ public final class AMEntitlement {
 
     public static final String SAML2_IDP_METADATA_READ = "SAML2_IDP_METADATA_READ";
 
+    public static final String SAML2_SP_METADATA_CREATE = "SAML2_SP_METADATA_CREATE";
+
+    public static final String SAML2_SP_METADATA_UPDATE = "SAML2_SP_METADATA_UPDATE";
+
+    public static final String SAML2_SP_METADATA_READ = "SAML2_SP_METADATA_READ";
+
+    public static final String SAML2_SP_KEYSTORE_CREATE = "SAML2_SP_KEYSTORE_CREATE";
+
+    public static final String SAML2_SP_KEYSTORE_UPDATE = "SAML2_SP_KEYSTORE_UPDATE";
+
+    public static final String SAML2_SP_KEYSTORE_READ = "SAML2_SP_KEYSTORE_READ";
+
     private static final Set<String> VALUES;
 
     static {
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPKeystoreConfService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPKeystoreConfService.java
new file mode 100644
index 0000000..ffc169e
--- /dev/null
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPKeystoreConfService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rest.api.service;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * REST operations for SAML 2.0 SP Keystore.
+ */
+@Tag(name = "SAML 2.0 SP Keystore")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer") })
+@Path("saml2sp/conf/keystore")
+public interface SAML2SPKeystoreConfService extends JAXRSService {
+
+    /**
+     * Updates SAML 2.0 SP keystore matching the given key.
+     *
+     * @param keystoreTO SAML2SPKeystoreTO to replace existing SAML 2.0 SP metadata
+     */
+    @Parameter(name = "key", description = "SAML2SPKeystoreTO's key", in = ParameterIn.PATH, schema =
+            @Schema(type = "string"))
+    @ApiResponses(
+            @ApiResponse(responseCode = "204", description = "Operation was successful"))
+    @PUT
+    @Path("{key}")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    void update(@NotNull SAML2SPKeystoreTO keystoreTO);
+
+}
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPMetadataConfService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPMetadataConfService.java
new file mode 100644
index 0000000..227a0d0
--- /dev/null
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2SPMetadataConfService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.rest.api.service;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.security.SecurityRequirements;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * REST operations for SAML 2.0 SP metadata.
+ */
+@Tag(name = "SAML 2.0 SP Metadata")
+@SecurityRequirements({
+    @SecurityRequirement(name = "BasicAuthentication"),
+    @SecurityRequirement(name = "Bearer")})
+@Path("saml2sp/conf/metadata")
+public interface SAML2SPMetadataConfService extends JAXRSService {
+
+    /**
+     * Updates SAML 2.0 SP metadata matching the given key.
+     *
+     * @param metadataTO SAML2SPMetadata to replace existing SAML 2.0 SP metadata
+     */
+    @Parameter(name = "key", description = "SAML2SPMetadata's key", in = ParameterIn.PATH, schema =
+    @Schema(type = "string"))
+    @ApiResponses(
+        @ApiResponse(responseCode = "204", description = "Operation was successful"))
+    @PUT
+    @Path("{key}")
+    @Consumes({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML})
+    @Produces({MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML})
+    void update(@NotNull SAML2SPMetadataTO metadataTO);
+
+}
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2IdPMetadataService.java
similarity index 94%
copy from common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
copy to common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2IdPMetadataService.java
index 7791372..00201b7 100644
--- a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2IdPMetadataService.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.rest.api.service;
+package org.apache.syncope.common.rest.api.service.wa;
 
 import io.swagger.v3.oas.annotations.headers.Header;
 import io.swagger.v3.oas.annotations.media.Schema;
@@ -39,6 +39,7 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
 
 /**
  * REST operations for SAML 2.0 IdP metadata.
@@ -47,7 +48,7 @@ import org.apache.syncope.common.rest.api.RESTHeaders;
 @SecurityRequirements({
     @SecurityRequirement(name = "BasicAuthentication"),
     @SecurityRequirement(name = "Bearer") })
-@Path("saml2idp/metadata")
+@Path("wa/saml2idp/metadata")
 public interface SAML2IdPMetadataService extends JAXRSService {
 
     /**
@@ -59,7 +60,7 @@ public interface SAML2IdPMetadataService extends JAXRSService {
      */
     @GET
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    SAML2IdPMetadataTO get(@QueryParam("appliesTo") @DefaultValue("Syncope") String appliesTo);
+    SAML2IdPMetadataTO getByOwner(@QueryParam("appliesTo") @DefaultValue("Syncope") String appliesTo);
 
     /**
      * Returns the SAML 2.0 IdP metadata matching the given key.
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPKeystoreService.java
similarity index 58%
copy from common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
copy to common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPKeystoreService.java
index 7791372..549af13 100644
--- a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPKeystoreService.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.rest.api.service;
+package org.apache.syncope.common.rest.api.service.wa;
 
 import io.swagger.v3.oas.annotations.headers.Header;
 import io.swagger.v3.oas.annotations.media.Schema;
@@ -25,9 +25,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.security.SecurityRequirements;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.Consumes;
-import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -37,61 +40,58 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.common.rest.api.RESTHeaders;
 
 /**
- * REST operations for SAML 2.0 IdP metadata.
+ * REST operations for SAML 2.0 IdP keystore.
  */
-@Tag(name = "SAML 2.0 IdP Metadata")
+@Tag(name = "SAML 2.0 SP Metadata Keystore")
 @SecurityRequirements({
     @SecurityRequirement(name = "BasicAuthentication"),
     @SecurityRequirement(name = "Bearer") })
-@Path("saml2idp/metadata")
-public interface SAML2IdPMetadataService extends JAXRSService {
+@Path("wa/saml2sp/keystore")
+public interface SAML2SPKeystoreService extends JAXRSService {
 
     /**
-     * Returns a document outlining keys and metadata of Syncope as SAML 2.0 IdP.
+     * Returns a document outlining keystore for Syncope as SAML 2.0 SP.
      *
-     * @param appliesTo indicates the SAML 2.0 IdP metadata document owner and applicability, where a value of 'Syncope'
-     * indicates the Syncope server as the global owner of the metadata and keys.
-     * @return SAML 2.0 IdP metadata
+     * @param name indicates the SAML 2.0 SP keystore document owner.
+     * @return SAML 2.0 SP keystore
      */
     @GET
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    SAML2IdPMetadataTO get(@QueryParam("appliesTo") @DefaultValue("Syncope") String appliesTo);
+    SAML2SPKeystoreTO getByOwner(@QueryParam("name") String name);
 
     /**
-     * Returns the SAML 2.0 IdP metadata matching the given key.
+     * Returns the SAML 2.0 SP keystore matching the given key.
      *
-     * @param key key of requested SAML 2.0 IdP metadata
-     * @return SAML 2.0 IdP metadata with matching id
+     * @param key key of requested SAML 2.0 SP keystore
+     * @return SAML 2.0 SP keystore with matching id
      */
     @GET
     @Path("{key}")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    SAML2IdPMetadataTO read(@NotNull @PathParam("key") String key);
+    SAML2SPKeystoreTO read(@NotNull @PathParam("key") String key);
 
     /**
-     * Store the metadata and keys to finalize the metadata generation process.
+     * Store the keystore to finalize the keystore generation process.
      *
-     * @param saml2IdPMetadataTO SAML2IdPMetadata to be created
-     * @return Response object featuring Location header of created SAML 2.0 IdP metadata
+     * @param keystoreTO SAML2SPMetadataKeystoreTO to be created
+     * @return Response object featuring Location header of created SAML 2.0 SP keystore
      */
     @ApiResponses({
         @ApiResponse(responseCode = "201",
-                description = "SAML2IdPMetadata successfully created", headers = {
-                    @Header(name = RESTHeaders.RESOURCE_KEY, schema =
-                            @Schema(type = "string"),
-                            description = "UUID generated for the entity created"),
-                    @Header(name = HttpHeaders.LOCATION, schema =
-                            @Schema(type = "string"),
-                            description = "URL of the entity created") }),
+            description = "SAML2SPKeystoreTO successfully created", headers = {
+            @Header(name = RESTHeaders.RESOURCE_KEY, schema =
+            @Schema(type = "string"),
+                description = "UUID generated for the entity created"),
+            @Header(name = HttpHeaders.LOCATION, schema =
+            @Schema(type = "string"),
+                description = "URL of the entity created") }),
         @ApiResponse(responseCode = "409",
-                description = "Metadata already existing") })
+            description = "Metadata already existing") })
     @POST
     @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    Response set(@NotNull SAML2IdPMetadataTO saml2IdPMetadataTO);
+    Response set(@NotNull SAML2SPKeystoreTO keystoreTO);
 
 }
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPMetadataService.java
similarity index 58%
rename from common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
rename to common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPMetadataService.java
index 7791372..24bb918 100644
--- a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/SAML2IdPMetadataService.java
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/wa/SAML2SPMetadataService.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.common.rest.api.service;
+package org.apache.syncope.common.rest.api.service.wa;
 
 import io.swagger.v3.oas.annotations.headers.Header;
 import io.swagger.v3.oas.annotations.media.Schema;
@@ -25,9 +25,12 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.security.SecurityRequirements;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
+
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.Consumes;
-import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -37,61 +40,58 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.common.rest.api.RESTHeaders;
 
 /**
- * REST operations for SAML 2.0 IdP metadata.
+ * REST operations for SAML 2.0 SP metadata.
  */
-@Tag(name = "SAML 2.0 IdP Metadata")
+@Tag(name = "SAML 2.0 SP Metadata")
 @SecurityRequirements({
     @SecurityRequirement(name = "BasicAuthentication"),
     @SecurityRequirement(name = "Bearer") })
-@Path("saml2idp/metadata")
-public interface SAML2IdPMetadataService extends JAXRSService {
+@Path("wa/saml2sp/metadata")
+public interface SAML2SPMetadataService extends JAXRSService {
 
     /**
-     * Returns a document outlining keys and metadata of Syncope as SAML 2.0 IdP.
+     * Returns a document outlining metadata for Syncope as SAML 2.0 SP.
      *
-     * @param appliesTo indicates the SAML 2.0 IdP metadata document owner and applicability, where a value of 'Syncope'
-     * indicates the Syncope server as the global owner of the metadata and keys.
-     * @return SAML 2.0 IdP metadata
+     * @param name indicates the SAML 2.0 SP metadata document owner.
+     * @return SAML 2.0 SP metadata
      */
     @GET
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    SAML2IdPMetadataTO get(@QueryParam("appliesTo") @DefaultValue("Syncope") String appliesTo);
+    SAML2SPMetadataTO getByOwner(@QueryParam("name") String name);
 
     /**
-     * Returns the SAML 2.0 IdP metadata matching the given key.
+     * Returns the SAML 2.0 SP metadata matching the given key.
      *
-     * @param key key of requested SAML 2.0 IdP metadata
-     * @return SAML 2.0 IdP metadata with matching id
+     * @param key key of requested SAML 2.0 SP metadata
+     * @return SAML 2.0 SP metadata with matching id
      */
     @GET
     @Path("{key}")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    SAML2IdPMetadataTO read(@NotNull @PathParam("key") String key);
+    SAML2SPMetadataTO read(@NotNull @PathParam("key") String key);
 
     /**
-     * Store the metadata and keys to finalize the metadata generation process.
+     * Store the metadata to finalize the metadata generation process.
      *
-     * @param saml2IdPMetadataTO SAML2IdPMetadata to be created
-     * @return Response object featuring Location header of created SAML 2.0 IdP metadata
+     * @param metadataTO SAML2SPMetadataTO to be created
+     * @return Response object featuring Location header of created SAML 2.0 SP metadata
      */
     @ApiResponses({
         @ApiResponse(responseCode = "201",
-                description = "SAML2IdPMetadata successfully created", headers = {
-                    @Header(name = RESTHeaders.RESOURCE_KEY, schema =
-                            @Schema(type = "string"),
-                            description = "UUID generated for the entity created"),
-                    @Header(name = HttpHeaders.LOCATION, schema =
-                            @Schema(type = "string"),
-                            description = "URL of the entity created") }),
+            description = "SAML2SPMetadata successfully created", headers = {
+            @Header(name = RESTHeaders.RESOURCE_KEY, schema =
+            @Schema(type = "string"),
+                description = "UUID generated for the entity created"),
+            @Header(name = HttpHeaders.LOCATION, schema =
+            @Schema(type = "string"),
+                description = "URL of the entity created") }),
         @ApiResponse(responseCode = "409",
-                description = "Metadata already existing") })
+            description = "Metadata already existing") })
     @POST
     @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    Response set(@NotNull SAML2IdPMetadataTO saml2IdPMetadataTO);
+    Response set(@NotNull SAML2SPMetadataTO metadataTO);
 
 }
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
new file mode 100644
index 0000000..103850c
--- /dev/null
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPKeystoreLogic.java
@@ -0,0 +1,113 @@
+/*
+ * 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 org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPKeystoreDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.provisioning.api.data.SAML2SPKeystoreBinder;
+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 java.lang.reflect.Method;
+
+@Component
+public class SAML2SPKeystoreLogic extends AbstractTransactionalLogic<SAML2SPKeystoreTO> {
+
+    @Autowired
+    private SAML2SPKeystoreBinder binder;
+
+    @Autowired
+    private SAML2SPKeystoreDAO saml2SPKeystoreDAO;
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_KEYSTORE_READ + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2SPKeystoreTO read(final String key) {
+        final SAML2SPKeystore keystore = saml2SPKeystoreDAO.find(key);
+        if (keystore == null) {
+            throw new NotFoundException(key + " not found");
+        }
+        return binder.getSAML2SPKeystoreTO(keystore);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_KEYSTORE_READ + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2SPKeystoreTO get(final String name) {
+        final SAML2SPKeystore keystore = saml2SPKeystoreDAO.findByOwner(name);
+        if (keystore == null) {
+            throw new NotFoundException("SAML2 SP keystore owned by " + name + " not found");
+        }
+
+        return binder.getSAML2SPKeystoreTO(keystore);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_KEYSTORE_CREATE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public SAML2SPKeystoreTO set(final SAML2SPKeystoreTO keystoreTO) {
+        final SAML2SPKeystore keystore = saml2SPKeystoreDAO.findByOwner(keystoreTO.getOwner());
+        if (keystore == null) {
+            return binder.getSAML2SPKeystoreTO(saml2SPKeystoreDAO.save(binder.create(keystoreTO)));
+        }
+        throw SyncopeClientException.build(ClientExceptionType.EntityExists);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_KEYSTORE_UPDATE + "')")
+    public SAML2SPKeystoreTO update(final SAML2SPKeystoreTO keystoreTO) {
+        final SAML2SPKeystore keystore = saml2SPKeystoreDAO.find(keystoreTO.getKey());
+        if (keystore == null) {
+            throw new NotFoundException(keystoreTO.getKey() + " not found");
+        }
+        return binder.getSAML2SPKeystoreTO(saml2SPKeystoreDAO.save(binder.update(keystore, keystoreTO)));
+    }
+
+    @Override
+    protected SAML2SPKeystoreTO resolveReference(final Method method, final Object... args)
+        throws UnresolvedReferenceException {
+        String name = null;
+        if (ArrayUtils.isNotEmpty(args)) {
+            for (int i = 0; name == null && i < args.length; i++) {
+                if (args[i] instanceof String) {
+                    name = (String) args[i];
+                } else if (args[i] instanceof SAML2SPKeystoreTO) {
+                    name = ((SAML2SPKeystoreTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (name != null) {
+            try {
+                return binder.getSAML2SPKeystoreTO(saml2SPKeystoreDAO.findByOwner(name));
+            } catch (final Throwable ignore) {
+                LOG.debug("Unresolved reference", ignore);
+                throw new UnresolvedReferenceException(ignore);
+            }
+        }
+        throw new UnresolvedReferenceException();
+    }
+}
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
new file mode 100644
index 0000000..09182bc
--- /dev/null
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/SAML2SPMetadataLogic.java
@@ -0,0 +1,113 @@
+/*
+ * 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 org.apache.commons.lang3.ArrayUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPMetadataDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+import org.apache.syncope.core.provisioning.api.data.SAML2SPMetadataBinder;
+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 java.lang.reflect.Method;
+
+@Component
+public class SAML2SPMetadataLogic extends AbstractTransactionalLogic<SAML2SPMetadataTO> {
+
+    @Autowired
+    private SAML2SPMetadataBinder binder;
+
+    @Autowired
+    private SAML2SPMetadataDAO saml2SPMetadataDAO;
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_METADATA_READ + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2SPMetadataTO read(final String key) {
+        final SAML2SPMetadata metadata = saml2SPMetadataDAO.find(key);
+        if (metadata == null) {
+            throw new NotFoundException(key + " not found");
+        }
+        return binder.getSAML2SPMetadataTO(metadata);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_METADATA_READ + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    @Transactional(readOnly = true)
+    public SAML2SPMetadataTO get(final String name) {
+        final SAML2SPMetadata metadata = saml2SPMetadataDAO.findByOwner(name);
+        if (metadata == null) {
+            throw new NotFoundException("SAML2 SP Metadata owned by " + name + " not found");
+        }
+
+        return binder.getSAML2SPMetadataTO(metadata);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_METADATA_CREATE + "') "
+        + "or hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
+    public SAML2SPMetadataTO set(final SAML2SPMetadataTO metadataTO) {
+        SAML2SPMetadata metadata = saml2SPMetadataDAO.findByOwner(metadataTO.getOwner());
+        if (metadata == null) {
+            return binder.getSAML2SPMetadataTO(saml2SPMetadataDAO.save(binder.create(metadataTO)));
+        }
+        throw SyncopeClientException.build(ClientExceptionType.EntityExists);
+    }
+
+    @PreAuthorize("hasRole('" + AMEntitlement.SAML2_SP_METADATA_UPDATE + "')")
+    public SAML2SPMetadataTO update(final SAML2SPMetadataTO metadataTO) {
+        final SAML2SPMetadata metadata = saml2SPMetadataDAO.find(metadataTO.getKey());
+        if (metadata == null) {
+            throw new NotFoundException(metadataTO.getKey() + " not found");
+        }
+        return binder.getSAML2SPMetadataTO(saml2SPMetadataDAO.save(binder.update(metadata, metadataTO)));
+    }
+
+    @Override
+    protected SAML2SPMetadataTO resolveReference(final Method method, final Object... args)
+        throws UnresolvedReferenceException {
+        String name = null;
+        if (ArrayUtils.isNotEmpty(args)) {
+            for (int i = 0; name == null && i < args.length; i++) {
+                if (args[i] instanceof String) {
+                    name = (String) args[i];
+                } else if (args[i] instanceof SAML2SPMetadataTO) {
+                    name = ((SAML2SPMetadataTO) args[i]).getKey();
+                }
+            }
+        }
+
+        if (name != null) {
+            try {
+                return binder.getSAML2SPMetadataTO(saml2SPMetadataDAO.findByOwner(name));
+            } catch (final Throwable ignore) {
+                LOG.debug("Unresolved reference", ignore);
+                throw new UnresolvedReferenceException(ignore);
+            }
+        }
+        throw new UnresolvedReferenceException();
+    }
+}
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPKeystoreConfServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPKeystoreConfServiceImpl.java
new file mode 100644
index 0000000..43aaedf
--- /dev/null
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPKeystoreConfServiceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.rest.cxf.service;
+
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.rest.api.service.SAML2SPKeystoreConfService;
+import org.apache.syncope.core.logic.SAML2SPKeystoreLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SAML2SPKeystoreConfServiceImpl extends AbstractServiceImpl implements SAML2SPKeystoreConfService {
+
+    @Autowired
+    private SAML2SPKeystoreLogic logic;
+
+    @Override
+    public void update(final SAML2SPKeystoreTO metadataTO) {
+        logic.update(metadataTO);
+    }
+}
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPMetadataConfServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPMetadataConfServiceImpl.java
new file mode 100644
index 0000000..13f9239
--- /dev/null
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2SPMetadataConfServiceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.rest.cxf.service;
+
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.SAML2SPMetadataConfService;
+import org.apache.syncope.core.logic.SAML2SPMetadataLogic;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class SAML2SPMetadataConfServiceImpl extends AbstractServiceImpl implements SAML2SPMetadataConfService {
+
+    @Autowired
+    private SAML2SPMetadataLogic logic;
+
+    @Override
+    public void update(final SAML2SPMetadataTO metadataTO) {
+        logic.update(metadataTO);
+    }
+}
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2IdPMetadataServiceImpl.java
similarity index 87%
copy from core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
copy to core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2IdPMetadataServiceImpl.java
index b1eb6a3..6bc3dc2 100644
--- a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2IdPMetadataServiceImpl.java
@@ -16,14 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.rest.cxf.service;
+package org.apache.syncope.core.rest.cxf.service.wa;
 
 import java.net.URI;
 import javax.ws.rs.core.Response;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2IdPMetadataService;
 import org.apache.syncope.core.logic.SAML2IdPMetadataLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -34,7 +35,7 @@ public class SAML2IdPMetadataServiceImpl extends AbstractServiceImpl implements
     private SAML2IdPMetadataLogic logic;
 
     @Override
-    public SAML2IdPMetadataTO get(final String appliesTo) {
+    public SAML2IdPMetadataTO getByOwner(final String appliesTo) {
         return logic.get(appliesTo);
     }
 
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPKeystoreServiceImpl.java
similarity index 56%
copy from core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
copy to core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPKeystoreServiceImpl.java
index b1eb6a3..852a701 100644
--- a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPKeystoreServiceImpl.java
@@ -16,39 +16,42 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.rest.cxf.service;
+package org.apache.syncope.core.rest.cxf.service.wa;
 
-import java.net.URI;
-import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
-import org.apache.syncope.core.logic.SAML2IdPMetadataLogic;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
+import org.apache.syncope.core.logic.SAML2SPKeystoreLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+
 @Service
-public class SAML2IdPMetadataServiceImpl extends AbstractServiceImpl implements SAML2IdPMetadataService {
+public class SAML2SPKeystoreServiceImpl extends AbstractServiceImpl implements SAML2SPKeystoreService {
 
     @Autowired
-    private SAML2IdPMetadataLogic logic;
+    private SAML2SPKeystoreLogic logic;
 
     @Override
-    public SAML2IdPMetadataTO get(final String appliesTo) {
-        return logic.get(appliesTo);
+    public SAML2SPKeystoreTO getByOwner(final String name) {
+        return logic.get(name);
     }
 
     @Override
-    public SAML2IdPMetadataTO read(final String key) {
+    public SAML2SPKeystoreTO read(final String key) {
         return logic.read(key);
     }
 
     @Override
-    public Response set(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
-        SAML2IdPMetadataTO saml2IdPMetadata = logic.set(saml2IdPMetadataTO);
-        URI location = uriInfo.getAbsolutePathBuilder().path(saml2IdPMetadata.getKey()).build();
+    public Response set(final SAML2SPKeystoreTO keystoreTO) {
+        final SAML2SPKeystoreTO keystore = logic.set(keystoreTO);
+        URI location = uriInfo.getAbsolutePathBuilder().path(keystore.getKey()).build();
         return Response.created(location).
-                header(RESTHeaders.RESOURCE_KEY, saml2IdPMetadata.getKey()).
-                build();
+            header(RESTHeaders.RESOURCE_KEY, keystore.getKey()).
+            build();
     }
 }
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPMetadataServiceImpl.java
similarity index 56%
rename from core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
rename to core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPMetadataServiceImpl.java
index b1eb6a3..3a8bf55 100644
--- a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/SAML2IdPMetadataServiceImpl.java
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/wa/SAML2SPMetadataServiceImpl.java
@@ -16,39 +16,42 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.rest.cxf.service;
+package org.apache.syncope.core.rest.cxf.service.wa;
 
-import java.net.URI;
-import javax.ws.rs.core.Response;
-import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
-import org.apache.syncope.core.logic.SAML2IdPMetadataLogic;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.core.logic.SAML2SPMetadataLogic;
+import org.apache.syncope.core.rest.cxf.service.AbstractServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+
 @Service
-public class SAML2IdPMetadataServiceImpl extends AbstractServiceImpl implements SAML2IdPMetadataService {
+public class SAML2SPMetadataServiceImpl extends AbstractServiceImpl implements SAML2SPMetadataService {
 
     @Autowired
-    private SAML2IdPMetadataLogic logic;
+    private SAML2SPMetadataLogic logic;
 
     @Override
-    public SAML2IdPMetadataTO get(final String appliesTo) {
-        return logic.get(appliesTo);
+    public SAML2SPMetadataTO getByOwner(final String name) {
+        return logic.get(name);
     }
 
     @Override
-    public SAML2IdPMetadataTO read(final String key) {
+    public SAML2SPMetadataTO read(final String key) {
         return logic.read(key);
     }
 
     @Override
-    public Response set(final SAML2IdPMetadataTO saml2IdPMetadataTO) {
-        SAML2IdPMetadataTO saml2IdPMetadata = logic.set(saml2IdPMetadataTO);
-        URI location = uriInfo.getAbsolutePathBuilder().path(saml2IdPMetadata.getKey()).build();
+    public Response set(final SAML2SPMetadataTO metadataTO) {
+        SAML2SPMetadataTO spMetadata = logic.set(metadataTO);
+        URI location = uriInfo.getAbsolutePathBuilder().path(spMetadata.getKey()).build();
         return Response.created(location).
-                header(RESTHeaders.RESOURCE_KEY, saml2IdPMetadata.getKey()).
-                build();
+            header(RESTHeaders.RESOURCE_KEY, spMetadata.getKey()).
+            build();
     }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPKeystoreDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPKeystoreDAO.java
new file mode 100644
index 0000000..5352a28
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPKeystoreDAO.java
@@ -0,0 +1,32 @@
+/*
+ * 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.persistence.api.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.DAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+
+public interface SAML2SPKeystoreDAO extends DAO<SAML2SPKeystore> {
+
+    SAML2SPKeystore find(String key);
+
+    SAML2SPKeystore findByOwner(String name);
+
+    SAML2SPKeystore save(SAML2SPKeystore keystore);
+
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPMetadataDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPMetadataDAO.java
new file mode 100644
index 0000000..ef1894f
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/auth/SAML2SPMetadataDAO.java
@@ -0,0 +1,32 @@
+/*
+ * 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.persistence.api.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.DAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+
+public interface SAML2SPMetadataDAO extends DAO<SAML2SPMetadata> {
+
+    SAML2SPMetadata find(String key);
+
+    SAML2SPMetadata findByOwner(String name);
+
+    SAML2SPMetadata save(SAML2SPMetadata metadata);
+
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPKeystore.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPKeystore.java
new file mode 100644
index 0000000..83fe696
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPKeystore.java
@@ -0,0 +1,32 @@
+/*
+ * 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.persistence.api.entity.auth;
+
+import org.apache.syncope.core.persistence.api.entity.Entity;
+
+public interface SAML2SPKeystore extends Entity {
+
+    String getKeystore();
+
+    void setKeystore(String keystore);
+
+    String getOwner();
+
+    void setOwner(String owner);
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPMetadata.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPMetadata.java
new file mode 100644
index 0000000..78b4c10
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/auth/SAML2SPMetadata.java
@@ -0,0 +1,32 @@
+/*
+ * 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.persistence.api.entity.auth;
+
+import org.apache.syncope.core.persistence.api.entity.Entity;
+
+public interface SAML2SPMetadata extends Entity {
+
+    String getMetadata();
+
+    void setMetadata(String metadata);
+
+    String getOwner();
+
+    void setOwner(String owner);
+}
diff --git a/core/persistence-jpa-json/pom.xml b/core/persistence-jpa-json/pom.xml
index da89ec5..83db64d 100644
--- a/core/persistence-jpa-json/pom.xml
+++ b/core/persistence-jpa-json/pom.xml
@@ -45,6 +45,16 @@ under the License.
     </dependency>
 
     <!-- TEST -->
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk15on</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency> 
       <groupId>javax.servlet</groupId> 
       <artifactId>javax.servlet-api</artifactId> 
@@ -125,6 +135,26 @@ under the License.
         <filtering>true</filtering>
       </resource>
     </resources>
+    <testResources>
+      <testResource>
+        <directory>src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+        <excludes>
+          <exclude>domains/MasterContent.xml</exclude>
+        </excludes>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../persistence-jpa/src/test/resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>*-metadata.xml</include>
+        </includes>
+      </testResource>
+    </testResources>
   </build>
 
   <profiles>
@@ -286,21 +316,10 @@ under the License.
         
         <testResources>
           <testResource>
-            <directory>src/test/resources</directory>
-            <filtering>true</filtering>
-          </testResource>
-          <testResource>
             <directory>src/test/resources/pgjsonb</directory>
             <filtering>true</filtering>
           </testResource>
           <testResource>
-            <directory>src/main/resources</directory>
-            <filtering>true</filtering>
-            <excludes>
-              <exclude>domains/MasterContent.xml</exclude>
-            </excludes>
-          </testResource>
-          <testResource>
             <directory>src/main/resources/pgjsonb</directory>
             <filtering>true</filtering>
             <excludes>
@@ -413,21 +432,10 @@ under the License.
         
         <testResources>
           <testResource>
-            <directory>src/test/resources</directory>
-            <filtering>true</filtering>
-          </testResource>
-          <testResource>
             <directory>src/test/resources/myjson</directory>
             <filtering>true</filtering>
           </testResource>
           <testResource>
-            <directory>src/main/resources</directory>
-            <filtering>true</filtering>
-            <excludes>
-              <exclude>domains/MasterContent.xml</exclude>
-            </excludes>
-          </testResource>
-          <testResource>
             <directory>src/main/resources/myjson</directory>
             <filtering>true</filtering>
             <excludes>
diff --git a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
index 1683d0b..3394661 100644
--- a/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa-json/src/test/resources/domains/MasterContent.xml
@@ -68,9 +68,10 @@ under the License.
   <AuthModule id="4c3ed4e6-7008-11ea-bc55-0242ac130003" name="DefaultGoogleMfaAuthModule"
               description="Google Mfa auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.GoogleMfaAuthModuleConf","name":"MyGoogleMfaAuthModuleConf","codeDigits":6,"issuer":"SyncopeTest"}'/>
   <AuthModule id="4c3ed8f6-7008-11ea-bc55-0242ac130003" name="DefaultOIDCAuthModule"
-              description="OIDC auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.OIDCAuthModuleConf","name":"MyOIDCAuthModuleConf", "discoveryUri":"www.testurl.com"}'/>
+              description="OIDC auth module"
+              jsonConf='{"@class":"org.apache.syncope.common.lib.auth.OIDCAuthModuleConf","name":"MyOIDCAuthModuleConf", "discoveryUri":"https://accounts.google.com/.well-known/openid-configuration", "id":"client-id", "secret": "client-secret" }'/>
   <AuthModule id="4c3ed9d2-7008-11ea-bc55-0242ac130003" name="DefaultSAML2IdPAuthModule"
-              description="SAML2 IdP auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.SAML2IdPAuthModuleConf","name":"MySAML2IdPAuthModuleConf", "providerName":"testProviderName","serviceProviderMetadataPath":"file:/etc/metadata"}'/>
+              description="SAML2 IdP auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.SAML2IdPAuthModuleConf","name":"MySAML2IdPAuthModuleConf", "keystorePassword":"p@$$word","privateKeyPassword":"p@$$word","identityProviderMetadataPath":"classpath:/idp-metadata.xml", "serviceProviderEntityId":"syncope:apache:org"}'/>
   <AuthModule id="4c3edbbc-7008-11ea-bc55-0242ac130003" name="DefaultJaasAuthModule"
               description="Jaas auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.JaasAuthModuleConf","name":"MyJaasAuthModuleConf","realm":"SYNCOPE","kerberosRealmSystemProperty":"sample-value"}'/>
   <AuthModule id="4c3edc98-7008-11ea-bc55-0242ac130003" name="DefaultStaticAuthModule"
diff --git a/core/persistence-jpa/pom.xml b/core/persistence-jpa/pom.xml
index 6f5114e..0d6b810 100644
--- a/core/persistence-jpa/pom.xml
+++ b/core/persistence-jpa/pom.xml
@@ -128,6 +128,16 @@ under the License.
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk15on</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPKeystoreDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPKeystoreDAO.java
new file mode 100644
index 0000000..c3b9761
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPKeystoreDAO.java
@@ -0,0 +1,62 @@
+/*
+ * 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.persistence.jpa.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPKeystoreDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPKeystore;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+
+@Repository
+public class JPASAML2SPKeystoreDAO extends AbstractDAO<SAML2SPKeystore> implements SAML2SPKeystoreDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2SPKeystore find(final String key) {
+        return entityManager().find(JPASAML2SPKeystore.class, key);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2SPKeystore findByOwner(final String owner) {
+        TypedQuery<SAML2SPKeystore> query = entityManager().createQuery(
+            "SELECT e FROM " + JPASAML2SPKeystore.class.getSimpleName() + " e WHERE e.owner=:owner",
+            SAML2SPKeystore.class);
+        query.setParameter("owner", owner);
+
+        SAML2SPKeystore result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug("No SAML2 SP Keystore found with appliesTo = {}", owner);
+        }
+        return result;
+    }
+
+    @Override
+    public SAML2SPKeystore save(final SAML2SPKeystore saml2IdPMetadata) {
+        return entityManager().merge(saml2IdPMetadata);
+    }
+
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPMetadataDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPMetadataDAO.java
new file mode 100644
index 0000000..21ee9bf
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/auth/JPASAML2SPMetadataDAO.java
@@ -0,0 +1,62 @@
+/*
+ * 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.persistence.jpa.dao.auth;
+
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPMetadataDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+import org.apache.syncope.core.persistence.jpa.dao.AbstractDAO;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPMetadata;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.NoResultException;
+import javax.persistence.TypedQuery;
+
+@Repository
+public class JPASAML2SPMetadataDAO extends AbstractDAO<SAML2SPMetadata> implements SAML2SPMetadataDAO {
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2SPMetadata find(final String key) {
+        return entityManager().find(JPASAML2SPMetadata.class, key);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public SAML2SPMetadata findByOwner(final String owner) {
+        TypedQuery<SAML2SPMetadata> query = entityManager().createQuery(
+            "SELECT e FROM " + JPASAML2SPMetadata.class.getSimpleName() + " e WHERE e.owner=:owner",
+            SAML2SPMetadata.class);
+        query.setParameter("owner", owner);
+
+        SAML2SPMetadata result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (final NoResultException e) {
+            LOG.debug("No SAML2 SP Metadata found with appliesTo = {}", owner);
+        }
+        return result;
+    }
+
+    @Override
+    public SAML2SPMetadata save(final SAML2SPMetadata saml2IdPMetadata) {
+        return entityManager().merge(saml2IdPMetadata);
+    }
+
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
index 28e6e66..7eafee6 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
@@ -61,6 +61,8 @@ import org.apache.syncope.core.persistence.api.entity.auth.AuthModuleItem;
 import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2IdPMetadata;
 import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
 import org.apache.syncope.core.persistence.api.entity.group.GPlainAttr;
 import org.apache.syncope.core.persistence.api.entity.group.GPlainAttrUniqueValue;
 import org.apache.syncope.core.persistence.api.entity.group.GPlainAttrValue;
@@ -112,6 +114,8 @@ import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPAOIDCRP;
 import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SP;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPKeystore;
+import org.apache.syncope.core.persistence.jpa.entity.auth.JPASAML2SPMetadata;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGPlainAttr;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGPlainAttrUniqueValue;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGPlainAttrValue;
@@ -325,6 +329,10 @@ public class JPAEntityFactory implements EntityFactory {
             result = (E) new JPASAML2SP();
         } else if (reference.equals(SAML2IdPMetadata.class)) {
             result = (E) new JPASAML2IdPMetadata();
+        } else if (reference.equals(SAML2SPMetadata.class)) {
+            result = (E) new JPASAML2SPMetadata();
+        } else if (reference.equals(SAML2SPKeystore.class)) {
+            result = (E) new JPASAML2SPKeystore();
         } else {
             throw new IllegalArgumentException("Could not find a JPA implementation of " + reference.getName());
         }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPKeystore.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPKeystore.java
new file mode 100644
index 0000000..b7ef84c
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPKeystore.java
@@ -0,0 +1,64 @@
+/*
+ * 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.persistence.jpa.entity.auth;
+
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Lob;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = JPASAML2SPKeystore.TABLE)
+public class JPASAML2SPKeystore extends AbstractGeneratedKeyEntity implements SAML2SPKeystore {
+
+    public static final String TABLE = "SAML2SPKeystore";
+
+    private static final long serialVersionUID = 22342617217394093L;
+
+    @Column(unique = true)
+    private String owner;
+
+    @Lob
+    @Column
+    private String keystore;
+
+    @Override
+    public String getKeystore() {
+        return keystore;
+    }
+
+    @Override
+    public void setKeystore(final String keystore) {
+        this.keystore = keystore;
+    }
+
+    @Override
+    public String getOwner() {
+        return owner;
+    }
+
+    @Override
+    public void setOwner(final String name) {
+        this.owner = name;
+    }
+
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPMetadata.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPMetadata.java
new file mode 100644
index 0000000..e43e5ac
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/auth/JPASAML2SPMetadata.java
@@ -0,0 +1,64 @@
+/*
+ * 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.persistence.jpa.entity.auth;
+
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+import org.apache.syncope.core.persistence.jpa.entity.AbstractGeneratedKeyEntity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Lob;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = JPASAML2SPMetadata.TABLE)
+public class JPASAML2SPMetadata extends AbstractGeneratedKeyEntity implements SAML2SPMetadata {
+
+    public static final String TABLE = "SAML2SPMetadata";
+
+    private static final long serialVersionUID = 12342617217394093L;
+
+    @Column(unique = true)
+    private String owner;
+
+    @Lob
+    @Column
+    private String metadata;
+
+    @Override
+    public String getMetadata() {
+        return metadata;
+    }
+
+    @Override
+    public void setMetadata(final String metadata) {
+        this.metadata = metadata;
+    }
+
+    @Override
+    public String getOwner() {
+        return owner;
+    }
+
+    @Override
+    public void setOwner(final String name) {
+        this.owner = name;
+    }
+
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthModuleTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthModuleTest.java
index 8bd2c13..730a67e 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthModuleTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AuthModuleTest.java
@@ -267,8 +267,6 @@ public class AuthModuleTest extends AbstractTest {
         SAML2IdPAuthModuleConf conf = new SAML2IdPAuthModuleConf();
         conf.setServiceProviderEntityId("testEntityId");
         conf.setProviderName("testProviderName");
-        conf.setServiceProviderMetadataPath("file:/etc/metadata");
-
         saveAuthModule("SAML2IdPAuthModuleTest", conf);
     }
 
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPKeystoreTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPKeystoreTest.java
new file mode 100644
index 0000000..9dfbcd4
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPKeystoreTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.persistence.jpa.inner;
+
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPKeystoreDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.ASN1Integer;
+import org.bouncycastle.asn1.DERBitString;
+import org.bouncycastle.asn1.DERNull;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.asn1.x509.Time;
+import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+import java.util.Date;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Transactional("Master")
+public class SAML2SPKeystoreTest extends AbstractTest {
+
+    @Autowired
+    private SAML2SPKeystoreDAO saml2SPKeystoreDAO;
+
+    private static Certificate createSelfSignedCert(final KeyPair keyPair) throws Exception {
+        final X500Name dn = new X500Name("cn=Unknown");
+        final V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
+
+        certGen.setSerialNumber(new ASN1Integer(BigInteger.valueOf(1)));
+        certGen.setIssuer(dn);
+        certGen.setSubject(dn);
+        certGen.setStartDate(new Time(new Date(System.currentTimeMillis() - 1000L)));
+
+        final Date expiration = new Date(System.currentTimeMillis() + 100000);
+        certGen.setEndDate(new Time(expiration));
+
+        final AlgorithmIdentifier sigAlgID = new AlgorithmIdentifier(PKCSObjectIdentifiers.sha1WithRSAEncryption, DERNull.INSTANCE);
+        certGen.setSignature(sigAlgID);
+        certGen.setSubjectPublicKeyInfo(SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
+
+        final Signature sig = Signature.getInstance("SHA1WithRSA");
+        sig.initSign(keyPair.getPrivate());
+        sig.update(certGen.generateTBSCertificate().getEncoded(ASN1Encoding.DER));
+
+        final TBSCertificate tbsCert = certGen.generateTBSCertificate();
+        final ASN1EncodableVector v = new ASN1EncodableVector();
+
+        v.add(tbsCert);
+        v.add(sigAlgID);
+        v.add(new DERBitString(sig.sign()));
+
+        final Certificate cert = CertificateFactory.getInstance("X.509")
+            .generateCertificate(new ByteArrayInputStream(new DERSequence(v).getEncoded(ASN1Encoding.DER)));
+        cert.verify(keyPair.getPublic());
+        return cert;
+    }
+
+    @Test
+    public void find() throws Exception {
+        create("Syncope");
+        SAML2SPKeystore saml2SPKeystore = saml2SPKeystoreDAO.findByOwner("Syncope");
+        assertNotNull(saml2SPKeystore);
+
+        saml2SPKeystore = saml2SPKeystoreDAO.findByOwner(UUID.randomUUID().toString());
+        assertNull(saml2SPKeystore);
+    }
+
+    @Test
+    public void save() throws Exception {
+        final SAML2SPKeystore keystore = create("SyncopeCreate");
+        final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        char[] pwdArray = "password".toCharArray();
+        ks.load(new ByteArrayInputStream(Base64.getDecoder().decode(keystore.getKeystore())), pwdArray);
+        assertTrue(ks.size() > 0);
+    }
+
+    @Test
+    public void update() throws Exception {
+        SAML2SPKeystore saml2SPKeystore = create("SyncopeUpdate");
+        assertNotNull(saml2SPKeystore);
+        saml2SPKeystore.setOwner("OtherSyncope");
+
+        saml2SPKeystore = saml2SPKeystoreDAO.save(saml2SPKeystore);
+        assertNotNull(saml2SPKeystore);
+        assertNotNull(saml2SPKeystore.getKey());
+        SAML2SPKeystore found = saml2SPKeystoreDAO.findByOwner(saml2SPKeystore.getOwner());
+        assertNotNull(found);
+        assertEquals("OtherSyncope", found.getOwner());
+    }
+
+    private SAML2SPKeystore create(final String owner) throws Exception {
+        final SAML2SPKeystore saml2SPKeystore = entityFactory.newEntity(SAML2SPKeystore.class);
+        saml2SPKeystore.setOwner(owner);
+
+        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        char[] pwdArray = "password".toCharArray();
+        ks.load(null, pwdArray);
+
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(4096);
+        KeyPair keyPair = keyPairGenerator.generateKeyPair();
+        Certificate certificate = createSelfSignedCert(keyPair);
+        ks.setKeyEntry("main", keyPair.getPrivate(), "password".toCharArray(), new Certificate[]{certificate});
+
+        try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) {
+            ks.store(fos, pwdArray);
+            fos.flush();
+            String keystore = Base64.getEncoder().encodeToString(fos.toByteArray());
+            saml2SPKeystore.setKeystore(keystore);
+        }
+        saml2SPKeystoreDAO.save(saml2SPKeystore);
+        assertNotNull(saml2SPKeystore);
+        assertNotNull(saml2SPKeystore.getKey());
+        assertNotNull(saml2SPKeystoreDAO.findByOwner(saml2SPKeystore.getOwner()));
+        return saml2SPKeystore;
+    }
+}
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPMetadataTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPMetadataTest.java
new file mode 100644
index 0000000..f6cbeae
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/SAML2SPMetadataTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.persistence.jpa.inner;
+
+import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPMetadataDAO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.apache.tika.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@Transactional("Master")
+public class SAML2SPMetadataTest extends AbstractTest {
+
+    @Autowired
+    private SAML2SPMetadataDAO saml2SPMetadataDAO;
+
+    @Test
+    public void find() throws Exception {
+        create("Syncope");
+        SAML2SPMetadata saml2SPMetadata = saml2SPMetadataDAO.findByOwner("Syncope");
+        assertNotNull(saml2SPMetadata);
+
+        saml2SPMetadata = saml2SPMetadataDAO.findByOwner(UUID.randomUUID().toString());
+        assertNull(saml2SPMetadata);
+    }
+
+    @Test
+    public void save() throws Exception {
+        create("SyncopeCreate");
+    }
+
+    @Test
+    public void update() throws Exception {
+        SAML2SPMetadata saml2SPMetadata = create("SyncopeUpdate");
+        assertNotNull(saml2SPMetadata);
+        saml2SPMetadata.setOwner("OtherSyncope");
+
+        saml2SPMetadata = saml2SPMetadataDAO.save(saml2SPMetadata);
+        assertNotNull(saml2SPMetadata);
+        assertNotNull(saml2SPMetadata.getKey());
+        SAML2SPMetadata found = saml2SPMetadataDAO.findByOwner(saml2SPMetadata.getOwner());
+        assertNotNull(found);
+        assertEquals("OtherSyncope", found.getOwner());
+    }
+
+    private SAML2SPMetadata create(final String owner) throws Exception {
+        SAML2SPMetadata saml2SPMetadata = entityFactory.newEntity(SAML2SPMetadata.class);
+        saml2SPMetadata.setOwner(owner);
+        String metadata = IOUtils.toString(new ClassPathResource("sp-metadata.xml").getInputStream());
+        saml2SPMetadata.setMetadata(metadata);
+        saml2SPMetadataDAO.save(saml2SPMetadata);
+        assertNotNull(saml2SPMetadata);
+        assertNotNull(saml2SPMetadata.getKey());
+        assertNotNull(saml2SPMetadataDAO.findByOwner(saml2SPMetadata.getOwner()));
+        return saml2SPMetadata;
+    }
+
+}
diff --git a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
index 8a5780c..7e1c33b 100644
--- a/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
+++ b/core/persistence-jpa/src/test/resources/domains/MasterContent.xml
@@ -68,9 +68,10 @@ under the License.
   <AuthModule id="4c3ed4e6-7008-11ea-bc55-0242ac130003" name="DefaultGoogleMfaAuthModule"
               description="Google Mfa auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.GoogleMfaAuthModuleConf","name":"MyGoogleMfaAuthModuleConf","codeDigits":6,"issuer":"SyncopeTest"}'/>
   <AuthModule id="4c3ed8f6-7008-11ea-bc55-0242ac130003" name="DefaultOIDCAuthModule"
-              description="OIDC auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.OIDCAuthModuleConf","name":"MyOIDCAuthModuleConf", "discoveryUri":"www.testurl.com", "id":"client-id", "secret": "client-secret" }'/>
+              description="OIDC auth module"
+              jsonConf='{"@class":"org.apache.syncope.common.lib.auth.OIDCAuthModuleConf","name":"MyOIDCAuthModuleConf", "discoveryUri":"https://accounts.google.com/.well-known/openid-configuration", "id":"client-id", "secret": "client-secret" }'/>
   <AuthModule id="4c3ed9d2-7008-11ea-bc55-0242ac130003" name="DefaultSAML2IdPAuthModule"
-              description="SAML2 IdP auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.SAML2IdPAuthModuleConf","name":"MySAML2IdPAuthModuleConf", "keystorePassword":"p@$$word","privateKeyPassword":"p@$$word","keystorePath":"file:/etc/metadata/keystore.jks","identityProviderMetadataPath":"file:/etc/metadata/idp.xml", "serviceProviderMetadataPath":"file:/etc/metadata/sp.xml", "serviceProviderEntityId":"syncope:apache:org"}'/>
+              description="SAML2 IdP auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.SAML2IdPAuthModuleConf","name":"MySAML2IdPAuthModuleConf", "keystorePassword":"p@$$word","privateKeyPassword":"p@$$word","identityProviderMetadataPath":"classpath:/idp-metadata.xml", "serviceProviderEntityId":"syncope:apache:org"}'/>
   <AuthModule id="4c3edbbc-7008-11ea-bc55-0242ac130003" name="DefaultJaasAuthModule"
               description="Jaas auth module" jsonConf='{"@class":"org.apache.syncope.common.lib.auth.JaasAuthModuleConf","name":"MyJaasAuthModuleConf","realm":"SYNCOPE","kerberosRealmSystemProperty":"sample-value", "loginConfigType": "JavaLoginConfig", "loginConfigurationFile": "file:/etc/jaas/login.conf"}'/>
   <AuthModule id="4c3edc98-7008-11ea-bc55-0242ac130003" name="DefaultStaticAuthModule"
diff --git a/core/persistence-jpa/src/test/resources/idp-metadata.xml b/core/persistence-jpa/src/test/resources/idp-metadata.xml
new file mode 100644
index 0000000..2e0162b
--- /dev/null
+++ b/core/persistence-jpa/src/test/resources/idp-metadata.xml
@@ -0,0 +1,144 @@
+<?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.
+  -->
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://syncope.apache.org/idp">
+    <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+                        BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+                        NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+                        ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+                        R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+                        BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+                        I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+                        EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+                        ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+                        9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+                        HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+                        L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+                        sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+                        K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
+                        +Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+                        9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+                        i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+                        zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <KeyDescriptor use="encryption">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUKymtgciRE6pWwDfrsI58qL9pQMgwDQYJKoZIhvcNAQEL
+                        BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+                        NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+                        ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKrT805sny3vlaYjTn
+                        +m6m3VbUoStvnacgwHH+orNFhHoV1HP2ndoH5BsEDB9tQYuyRbGUm/nYVOSHayzB
+                        G3bzMGdU7woO6rsCqpHuxUyEvojd/y/N/r9jMzeBOCu0KDBTrn3BJhnGSwSTfhOS
+                        3r20JFmDuTkHmabRs7ro0BvDaQ29jh38ro1iwB4E/4mqb1zYP13NI3ooErN/o6pl
+                        XKpnFY37bDDOyOuocjN9tfPNIANNFKah0HjWOP0Nso0D1g6jHOSzmOw/Yxg61vBk
+                        qOD4aKhLYPAxsXRl80nDrwTnm3/9xLQj9D3uLAtDLnn9pSqn3jCLxsxsHfKL/zkB
+                        IKEBAgMBAAGjYzBhMB0GA1UdDgQWBBSrPjAgCJIHYmsofDcDIPzEhnYxmTBABgNV
+                        HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+                        L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAI8MlofbE0tbq8ez2d0Lq
+                        Syhp4Q/shMEwjqcDarOwR+ACB9McOannUpAG7TCDp8Ch5E/V1B0Uo/5DF2tAzB1y
+                        7sgAmy2mY9/mFhMYpOqTCagufwewaMkn9n7ETzC/6vQEjYrjiNyNR0F3UQQz2bhe
+                        ROM3YuKctuOnMthc+ZE7vn+AXCGumRHBhyCaYdzfeUh7id+yrd9B51+o3iF4eu6w
+                        zJi5z7FMCS6I4PSc/uWYDw1ahzoPONjazWSEWGUibZaJYM3pJHkuwqyWKOFGVknH
+                        J1Qv4WCfSPb6eva94TZX0lkLM01C7NZObnfxY3fvJGcyFl8wlRTUYvuqM8md5CEp
+                        LA==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+
+        <!--
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+                                   Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        -->
+
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SLO" />
+
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/ECP"/>
+    </IDPSSODescriptor>
+
+    <!--
+    <AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
++Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/AttributeQuery"/>
+    </AttributeAuthorityDescriptor>
+    -->
+
+    <!--
+    <Organization>
+        <OrganizationName xml:lang="en">Institution Name</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">Institution DisplayName</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">URL</OrganizationURL>
+    </Organization>
+    <ContactPerson contactType="administrative">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="technical">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="support">
+        <GivenName>IT Services Support</GivenName>
+        <EmailAddress>support@example.org</EmailAddress>
+    </ContactPerson>
+    -->
+</EntityDescriptor>
diff --git a/core/persistence-jpa/src/test/resources/sp-metadata.xml b/core/persistence-jpa/src/test/resources/sp-metadata.xml
new file mode 100644
index 0000000..351d556
--- /dev/null
+++ b/core/persistence-jpa/src/test/resources/sp-metadata.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="_lipmbmwyb3ujt0osvfect39lzglxnsdi7bxhvf5" entityID="urn:mace:saml:pac4j.org" validUntil="2038-08-10T19:43:10.846Z">
+    <md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport">
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha224"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
+    </md:Extensions>
+    <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
+        <md:Extensions xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init">
+            <init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://mmoayyed.unicon.net:8443/cas/login?client_name=SAML2Client"/>
+        </md:Extensions>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
+        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mmoayyed.unicon.net:8443/cas/login?client_name=SAML2Client" index="0"/>
+    </md:SPSSODescriptor>
+</md:EntityDescriptor>
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPKeystoreBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPKeystoreBinder.java
new file mode 100644
index 0000000..e0cc306
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPKeystoreBinder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+
+public interface SAML2SPKeystoreBinder {
+
+    SAML2SPKeystore create(SAML2SPKeystoreTO keystoreTO);
+
+    SAML2SPKeystore update(SAML2SPKeystore keystore, SAML2SPKeystoreTO keystoreTO);
+
+    SAML2SPKeystoreTO getSAML2SPKeystoreTO(SAML2SPKeystore keystore);
+
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPMetadataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPMetadataBinder.java
new file mode 100644
index 0000000..c49d64f
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/SAML2SPMetadataBinder.java
@@ -0,0 +1,32 @@
+/*
+ * 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.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+
+public interface SAML2SPMetadataBinder {
+
+    SAML2SPMetadata create(SAML2SPMetadataTO metadataTO);
+
+    SAML2SPMetadata update(SAML2SPMetadata metadata, SAML2SPMetadataTO metadataTO);
+
+    SAML2SPMetadataTO getSAML2SPMetadataTO(SAML2SPMetadata metadata);
+
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPKeystoreBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPKeystoreBinderImpl.java
new file mode 100644
index 0000000..02fd805
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPKeystoreBinderImpl.java
@@ -0,0 +1,71 @@
+/*
+ * 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.provisioning.java.data;
+
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPKeystore;
+import org.apache.syncope.core.provisioning.api.data.SAML2SPKeystoreBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SAML2SPKeystoreBinderImpl implements SAML2SPKeystoreBinder {
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    private SAML2SPKeystore getSAML2SPKeystore(
+        final SAML2SPKeystore keystore,
+        final SAML2SPKeystoreTO keystoreTO) {
+
+        SAML2SPKeystore result = keystore;
+        if (result == null) {
+            result = entityFactory.newEntity(SAML2SPKeystore.class);
+        }
+        result.setKeystore(keystoreTO.getKeystore());
+        result.setOwner(keystoreTO.getOwner());
+
+        return result;
+    }
+
+    @Override
+    public SAML2SPKeystore create(final SAML2SPKeystoreTO keystoreTO) {
+        return update(entityFactory.newEntity(SAML2SPKeystore.class), keystoreTO);
+    }
+
+    @Override
+    public SAML2SPKeystore update(
+        final SAML2SPKeystore keystore,
+        final SAML2SPKeystoreTO keystoreTO) {
+
+        return getSAML2SPKeystore(keystore, keystoreTO);
+    }
+
+    @Override
+    public SAML2SPKeystoreTO getSAML2SPKeystoreTO(final SAML2SPKeystore keystore) {
+        SAML2SPKeystoreTO keystoreTO = new SAML2SPKeystoreTO();
+
+        keystoreTO.setKey(keystore.getKey());
+        keystoreTO.setKeystore(keystore.getKeystore());
+        keystoreTO.setOwner(keystore.getOwner());
+
+        return keystoreTO;
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPMetadataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPMetadataBinderImpl.java
new file mode 100644
index 0000000..3e4e62b
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SPMetadataBinderImpl.java
@@ -0,0 +1,72 @@
+/*
+ * 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.provisioning.java.data;
+
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.auth.SAML2SPMetadata;
+import org.apache.syncope.core.provisioning.api.data.SAML2SPMetadataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SAML2SPMetadataBinderImpl implements SAML2SPMetadataBinder {
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    private SAML2SPMetadata getSAML2SPMetadata(
+        final SAML2SPMetadata metadata,
+        final SAML2SPMetadataTO metadataTO) {
+
+        SAML2SPMetadata result = metadata;
+        if (result == null) {
+            result = entityFactory.newEntity(SAML2SPMetadata.class);
+        }
+
+        result.setMetadata(metadataTO.getMetadata());
+        result.setOwner(metadataTO.getOwner());
+
+        return result;
+    }
+
+    @Override
+    public SAML2SPMetadata create(final SAML2SPMetadataTO metadataTO) {
+        return update(entityFactory.newEntity(SAML2SPMetadata.class), metadataTO);
+    }
+
+    @Override
+    public SAML2SPMetadata update(
+        final SAML2SPMetadata metadata,
+        final SAML2SPMetadataTO metadataTO) {
+
+        return getSAML2SPMetadata(metadata, metadataTO);
+    }
+
+    @Override
+    public SAML2SPMetadataTO getSAML2SPMetadataTO(final SAML2SPMetadata saml2IdPMetadata) {
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO();
+
+        metadataTO.setKey(saml2IdPMetadata.getKey());
+        metadataTO.setMetadata(saml2IdPMetadata.getMetadata());
+        metadataTO.setOwner(saml2IdPMetadata.getOwner());
+
+        return metadataTO;
+    }
+}
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index f6c9016..8a3a33c 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -222,6 +222,11 @@ under the License.
       <artifactId>bcpkix-jdk15on</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -788,6 +793,16 @@ under the License.
           <version>${jdbc.postgresql.version}</version>
           <scope>test</scope>
         </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk15on</artifactId>
+          <scope>test</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk15on</artifactId>
+          <scope>test</scope>
+        </dependency>
       </dependencies>
 
       <build>
@@ -1060,6 +1075,16 @@ under the License.
           <version>${jdbc.mysql.version}</version>
           <scope>test</scope>
         </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcpkix-jdk15on</artifactId>
+          <scope>test</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.bouncycastle</groupId>
+          <artifactId>bcprov-jdk15on</artifactId>
+          <scope>test</scope>
+        </dependency>
       </dependencies>
 
       <build>
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 91c7dcf..01936ca 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -63,11 +63,12 @@ import org.apache.syncope.common.lib.policy.PolicyTO;
 import org.apache.syncope.common.lib.request.AnyObjectCR;
 import org.apache.syncope.common.lib.request.GroupCR;
 import org.apache.syncope.common.lib.request.UserCR;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
 import org.apache.syncope.common.lib.to.SchemaTO;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.policy.AccessPolicyTO;
-import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.policy.AllowedAttrReleasePolicyConf;
 import org.apache.syncope.common.lib.policy.AttrReleasePolicyTO;
 import org.apache.syncope.common.lib.policy.DefaultAccessPolicyConf;
@@ -84,13 +85,11 @@ import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.to.AuthModuleTO;
 import org.apache.syncope.common.lib.policy.AuthPolicyTO;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.common.lib.to.ImplementationTO;
 import org.apache.syncope.common.lib.to.client.ClientAppTO;
 import org.apache.syncope.common.lib.to.client.OIDCRPTO;
 import org.apache.syncope.common.lib.to.client.SAML2SPTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientAppType;
-import org.apache.syncope.common.lib.types.ImplementationEngine;
 import org.apache.syncope.common.lib.types.OIDCSubjectType;
 import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.lib.types.PolicyType;
@@ -111,6 +110,10 @@ import org.apache.syncope.common.rest.api.service.ConnectorService;
 import org.apache.syncope.common.rest.api.service.DynRealmService;
 import org.apache.syncope.common.rest.api.service.LoggerService;
 import org.apache.syncope.common.rest.api.service.NotificationService;
+import org.apache.syncope.common.rest.api.service.SAML2SPKeystoreConfService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
+import org.apache.syncope.common.rest.api.service.SAML2SPMetadataConfService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
 import org.apache.syncope.common.rest.api.service.SAML2SPService;
 import org.apache.syncope.common.rest.api.service.PolicyService;
 import org.apache.syncope.common.rest.api.service.ReportService;
@@ -138,9 +141,8 @@ import org.apache.syncope.common.rest.api.service.UserRequestService;
 import org.apache.syncope.common.rest.api.service.BpmnProcessService;
 import org.apache.syncope.common.rest.api.service.GatewayRouteService;
 import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataConfService;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2IdPMetadataService;
 import org.apache.syncope.common.rest.api.service.UserWorkflowTaskService;
-import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.fit.core.CoreITContext;
 import org.apache.syncope.fit.core.UserITCase;
 import org.identityconnectors.common.security.Encryptor;
@@ -292,6 +294,14 @@ public abstract class AbstractITCase {
 
     protected static AuthModuleService authModuleService;
 
+    protected static SAML2SPMetadataService saml2SPMetadataService;
+
+    protected static SAML2SPMetadataConfService saml2SPMetadataConfService;
+
+    protected static SAML2SPKeystoreService saml2SPKeystoreService;
+
+    protected static SAML2SPKeystoreConfService saml2SPKeystoreConfService;
+
     protected static SAML2IdPMetadataService saml2IdPMetadataService;
 
     protected static SAML2IdPMetadataConfService saml2IdPMetadataConfService;
@@ -382,15 +392,19 @@ public abstract class AbstractITCase {
         remediationService = adminClient.getService(RemediationService.class);
         gatewayRouteService = adminClient.getService(GatewayRouteService.class);
         camelRouteService = adminClient.getService(CamelRouteService.class);
-        saml2SpService = adminClient.getService(org.apache.syncope.common.rest.api.service.SAML2SPService.class);
+        saml2SpService = adminClient.getService(SAML2SPService.class);
         saml2IdPService = adminClient.getService(SAML2IdPService.class);
         oidcClientService = adminClient.getService(OIDCClientService.class);
         oidcProviderService = adminClient.getService(OIDCProviderService.class);
         scimConfService = adminClient.getService(SCIMConfService.class);
         clientAppService = adminClient.getService(ClientAppService.class);
         authModuleService = adminClient.getService(AuthModuleService.class);
+        saml2SPMetadataService = adminClient.getService(SAML2SPMetadataService.class);
+        saml2SPMetadataConfService = adminClient.getService(SAML2SPMetadataConfService.class);
         saml2IdPMetadataService = adminClient.getService(SAML2IdPMetadataService.class);
         saml2IdPMetadataConfService = adminClient.getService(SAML2IdPMetadataConfService.class);
+        saml2SPKeystoreService = adminClient.getService(SAML2SPKeystoreService.class);
+        saml2SPKeystoreConfService = adminClient.getService(SAML2SPKeystoreConfService.class);
     }
 
     @Autowired
@@ -617,6 +631,28 @@ public abstract class AbstractITCase {
         return getObject(response.getLocation(), SAML2IdPMetadataService.class, saml2IdPMetadata.getClass());
     }
 
+    protected SAML2SPMetadataTO createSAML2SPMetadata(final SAML2SPMetadataTO metadata) {
+        Response response = saml2SPMetadataService.set(metadata);
+        if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
+            Exception ex = clientFactory.getExceptionMapper().fromResponse(response);
+            if (ex != null) {
+                throw (RuntimeException) ex;
+            }
+        }
+        return getObject(response.getLocation(), SAML2SPMetadataService.class, metadata.getClass());
+    }
+
+    protected SAML2SPKeystoreTO createSAML2SPKeystore(final SAML2SPKeystoreTO keystoreTO) {
+        Response response = saml2SPKeystoreService.set(keystoreTO);
+        if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
+            Exception ex = clientFactory.getExceptionMapper().fromResponse(response);
+            if (ex != null) {
+                throw (RuntimeException) ex;
+            }
+        }
+        return getObject(response.getLocation(), SAML2SPKeystoreService.class, keystoreTO.getClass());
+    }
+
     protected ResourceTO createResource(final ResourceTO resourceTO) {
         Response response = resourceService.create(resourceTO);
         if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthModuleITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthModuleITCase.java
index ab51c1f..ba5fc5a 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthModuleITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AuthModuleITCase.java
@@ -126,8 +126,6 @@ public class AuthModuleITCase extends AbstractITCase {
                 SAML2IdPAuthModuleConf.class.cast(conf).setName("TestConf" + getUUIDString());
                 SAML2IdPAuthModuleConf.class.cast(conf).setServiceProviderEntityId("testEntityId");
                 SAML2IdPAuthModuleConf.class.cast(conf).setProviderName("testProviderName");
-                SAML2IdPAuthModuleConf.class.cast(conf).setServiceProviderMetadataPath(
-                        "file:/etc/metadata");
                 break;
 
             case SYNCOPE:
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.java
index 58facb8..2971cb7 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2IdPMetadataITCase.java
@@ -63,7 +63,7 @@ public class SAML2IdPMetadataITCase extends AbstractITCase {
     public void read() {
         SAML2IdPMetadataTO saml2IdPMetadataTO = null;
         try {
-            saml2IdPMetadataTO = saml2IdPMetadataService.get(APPLIES_TO);
+            saml2IdPMetadataTO = saml2IdPMetadataService.getByOwner(APPLIES_TO);
         } catch (SyncopeClientException e) {
             saml2IdPMetadataTO = createSAML2IdPMetadata();
         }
@@ -76,7 +76,7 @@ public class SAML2IdPMetadataITCase extends AbstractITCase {
     @Test
     public void create() {
         try {
-            saml2IdPMetadataService.get(APPLIES_TO);
+            saml2IdPMetadataService.getByOwner(APPLIES_TO);
         } catch (SyncopeClientException e) {
             createSAML2IdPMetadata();
         }
@@ -96,7 +96,7 @@ public class SAML2IdPMetadataITCase extends AbstractITCase {
     public void update() {
         SAML2IdPMetadataTO saml2IdPMetadataTO = null;
         try {
-            saml2IdPMetadataTO = saml2IdPMetadataService.get(APPLIES_TO);
+            saml2IdPMetadataTO = saml2IdPMetadataService.getByOwner(APPLIES_TO);
         } catch (NotFoundException e) {
             saml2IdPMetadataTO = createSAML2IdPMetadata();
         }
@@ -104,7 +104,7 @@ public class SAML2IdPMetadataITCase extends AbstractITCase {
         assertNotNull(saml2IdPMetadataTO);
         saml2IdPMetadataTO.setEncryptionKey("newKey");
         saml2IdPMetadataConfService.update(saml2IdPMetadataTO);
-        saml2IdPMetadataTO = saml2IdPMetadataService.get(saml2IdPMetadataTO.getAppliesTo());
+        saml2IdPMetadataTO = saml2IdPMetadataService.getByOwner(saml2IdPMetadataTO.getAppliesTo());
         assertNotNull(saml2IdPMetadataTO);
 
         assertEquals("newKey", saml2IdPMetadataTO.getEncryptionKey());
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPKeystoreITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPKeystoreITCase.java
new file mode 100644
index 0000000..cc7e40c
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPKeystoreITCase.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.fit.core;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.StringUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SAML2SPKeystoreITCase extends AbstractITCase {
+
+    private static final String OWNER = "Syncope";
+
+    private static void testIsValid(final SAML2SPKeystoreTO keystoreTO) {
+        assertFalse(StringUtils.isBlank(keystoreTO.getOwner()));
+        assertFalse(StringUtils.isBlank(keystoreTO.getKeystore()));
+    }
+
+    private SAML2SPKeystoreTO createSAML2SPKeystore() {
+        SAML2SPKeystoreTO result = createSAML2SPKeystore(new SAML2SPKeystoreTO.Builder().
+            owner(OWNER).
+            keystore("testkyStore").
+            build());
+        assertNotNull(result);
+        testIsValid(result);
+        return result;
+    }
+
+    @Test
+    public void read() {
+        SAML2SPKeystoreTO keystoreTO;
+        try {
+            keystoreTO = saml2SPKeystoreService.getByOwner(OWNER);
+        } catch (SyncopeClientException e) {
+            keystoreTO = createSAML2SPKeystore();
+        }
+        assertNotNull(keystoreTO);
+        assertEquals(OWNER, keystoreTO.getOwner());
+        testIsValid(keystoreTO);
+    }
+
+    @Test
+    public void create() {
+        try {
+            saml2SPMetadataService.getByOwner(OWNER);
+        } catch (SyncopeClientException e) {
+            createSAML2SPKeystore();
+        }
+
+        try {
+            createSAML2SPKeystore(new SAML2SPKeystoreTO.Builder().
+                owner(OWNER).
+                keystore("testMetadata").
+                build());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.EntityExists, e.getType());
+        }
+    }
+
+    @Test
+    public void update() {
+        SAML2SPKeystoreTO keystoreTO;
+        try {
+            keystoreTO = saml2SPKeystoreService.getByOwner(OWNER);
+        } catch (NotFoundException e) {
+            keystoreTO = createSAML2SPKeystore();
+        }
+        assertNotNull(keystoreTO);
+        keystoreTO.setKeystore("new-keystore");
+        keystoreTO.setOwner("Syncope4");
+
+        saml2SPKeystoreConfService.update(keystoreTO);
+        keystoreTO = saml2SPKeystoreService.read(keystoreTO.getKey());
+        assertNotNull(keystoreTO);
+        assertEquals("new-keystore", keystoreTO.getKeystore());
+        assertEquals("Syncope4", keystoreTO.getOwner());
+    }
+
+}
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPMetadataITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPMetadataITCase.java
new file mode 100644
index 0000000..3fe0f0d
--- /dev/null
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SAML2SPMetadataITCase.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.fit.core;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.fit.AbstractITCase;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.util.StringUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class SAML2SPMetadataITCase extends AbstractITCase {
+
+    private static final String OWNER = "Syncope";
+
+    private static void testIsValid(final SAML2SPMetadataTO metadataTO) {
+        assertFalse(StringUtils.isBlank(metadataTO.getOwner()));
+        assertFalse(StringUtils.isBlank(metadataTO.getMetadata()));
+    }
+
+    private SAML2SPMetadataTO createSAML2SPMetadata() {
+        SAML2SPMetadataTO result = createSAML2SPMetadata(new SAML2SPMetadataTO.Builder().
+            owner(OWNER).
+            metadata("testMetadata").
+            build());
+        assertNotNull(result);
+        testIsValid(result);
+        return result;
+    }
+
+    @Test
+    public void read() {
+        SAML2SPMetadataTO metadataTO;
+        try {
+            metadataTO = saml2SPMetadataService.getByOwner(OWNER);
+        } catch (SyncopeClientException e) {
+            metadataTO = createSAML2SPMetadata();
+        }
+        assertNotNull(metadataTO);
+        assertEquals(OWNER, metadataTO.getOwner());
+        testIsValid(metadataTO);
+    }
+
+    @Test
+    public void create() {
+        try {
+            saml2SPMetadataService.getByOwner(OWNER);
+        } catch (SyncopeClientException e) {
+            createSAML2SPMetadata();
+        }
+
+        try {
+            createSAML2SPMetadata(new SAML2SPMetadataTO.Builder().
+                owner(OWNER).
+                metadata("testMetadata").
+                build());
+            fail("This should not happen");
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.EntityExists, e.getType());
+        }
+    }
+
+    @Test
+    public void update() {
+        SAML2SPMetadataTO metadataTO;
+        try {
+            metadataTO = saml2SPMetadataService.getByOwner(OWNER);
+        } catch (NotFoundException e) {
+            metadataTO = createSAML2SPMetadata();
+        }
+        assertNotNull(metadataTO);
+        metadataTO.setMetadata("new-metadata");
+        metadataTO.setOwner("Syncope4");
+
+        saml2SPMetadataConfService.update(metadataTO);
+        metadataTO = saml2SPMetadataService.read(metadataTO.getKey());
+        assertNotNull(metadataTO);
+        assertEquals("new-metadata", metadataTO.getMetadata());
+        assertEquals("Syncope4", metadataTO.getOwner());
+    }
+
+}
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
index 336b74f..0e5e122 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java
@@ -44,7 +44,6 @@ import org.apache.syncope.common.lib.request.MembershipUR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.request.AttrPatch;
-import org.apache.syncope.common.lib.search.UserPartialCondition;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
 import org.apache.syncope.common.lib.to.AnyTypeTO;
 import org.apache.syncope.common.lib.to.PagedResult;
diff --git a/fit/wa-reference/src/main/resources/idp-metadata.xml b/fit/wa-reference/src/main/resources/idp-metadata.xml
new file mode 100644
index 0000000..2e0162b
--- /dev/null
+++ b/fit/wa-reference/src/main/resources/idp-metadata.xml
@@ -0,0 +1,144 @@
+<?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.
+  -->
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://syncope.apache.org/idp">
+    <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+                        BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+                        NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+                        ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+                        R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+                        BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+                        I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+                        EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+                        ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+                        9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+                        HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+                        L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+                        sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+                        K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
+                        +Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+                        9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+                        i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+                        zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <KeyDescriptor use="encryption">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUKymtgciRE6pWwDfrsI58qL9pQMgwDQYJKoZIhvcNAQEL
+                        BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+                        NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+                        ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKrT805sny3vlaYjTn
+                        +m6m3VbUoStvnacgwHH+orNFhHoV1HP2ndoH5BsEDB9tQYuyRbGUm/nYVOSHayzB
+                        G3bzMGdU7woO6rsCqpHuxUyEvojd/y/N/r9jMzeBOCu0KDBTrn3BJhnGSwSTfhOS
+                        3r20JFmDuTkHmabRs7ro0BvDaQ29jh38ro1iwB4E/4mqb1zYP13NI3ooErN/o6pl
+                        XKpnFY37bDDOyOuocjN9tfPNIANNFKah0HjWOP0Nso0D1g6jHOSzmOw/Yxg61vBk
+                        qOD4aKhLYPAxsXRl80nDrwTnm3/9xLQj9D3uLAtDLnn9pSqn3jCLxsxsHfKL/zkB
+                        IKEBAgMBAAGjYzBhMB0GA1UdDgQWBBSrPjAgCJIHYmsofDcDIPzEhnYxmTBABgNV
+                        HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+                        L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAI8MlofbE0tbq8ez2d0Lq
+                        Syhp4Q/shMEwjqcDarOwR+ACB9McOannUpAG7TCDp8Ch5E/V1B0Uo/5DF2tAzB1y
+                        7sgAmy2mY9/mFhMYpOqTCagufwewaMkn9n7ETzC/6vQEjYrjiNyNR0F3UQQz2bhe
+                        ROM3YuKctuOnMthc+ZE7vn+AXCGumRHBhyCaYdzfeUh7id+yrd9B51+o3iF4eu6w
+                        zJi5z7FMCS6I4PSc/uWYDw1ahzoPONjazWSEWGUibZaJYM3pJHkuwqyWKOFGVknH
+                        J1Qv4WCfSPb6eva94TZX0lkLM01C7NZObnfxY3fvJGcyFl8wlRTUYvuqM8md5CEp
+                        LA==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+
+        <!--
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding"
+                                   Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        -->
+
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SLO" />
+
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/ECP"/>
+    </IDPSSODescriptor>
+
+    <!--
+    <AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
++Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/AttributeQuery"/>
+    </AttributeAuthorityDescriptor>
+    -->
+
+    <!--
+    <Organization>
+        <OrganizationName xml:lang="en">Institution Name</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">Institution DisplayName</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">URL</OrganizationURL>
+    </Organization>
+    <ContactPerson contactType="administrative">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="technical">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="support">
+        <GivenName>IT Services Support</GivenName>
+        <EmailAddress>support@example.org</EmailAddress>
+    </ContactPerson>
+    -->
+</EntityDescriptor>
diff --git a/pom.xml b/pom.xml
index 623d9f4..f1d5941 100644
--- a/pom.xml
+++ b/pom.xml
@@ -405,6 +405,7 @@ under the License.
     <connid.servicenow.version>1.0.0</connid.servicenow.version>
 
     <cxf.version>3.3.6</cxf.version>
+    <bouncycastle.version>1.65</bouncycastle.version>
 
     <jackson.version>2.11.0</jackson.version>
 
@@ -1625,6 +1626,11 @@ under the License.
       </dependency>
       <dependency>
         <groupId>org.apereo.cas</groupId>
+        <artifactId>cas-server-support-pac4j-core-clients</artifactId>
+        <version>${cas.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apereo.cas</groupId>
         <artifactId>cas-server-support-consent-webflow</artifactId>
         <version>${cas.version}</version>
       </dependency>
@@ -1984,7 +1990,12 @@ under the License.
       <dependency>
         <groupId>org.bouncycastle</groupId>
         <artifactId>bcpkix-jdk15on</artifactId>
-        <version>1.65</version>
+        <version>${bouncycastle.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.bouncycastle</groupId>
+        <artifactId>bcprov-jdk15on</artifactId>
+        <version>${bouncycastle.version}</version>
       </dependency>
 
       <!-- TEST -->
diff --git a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
index f4190b0..a93aa9d 100644
--- a/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
+++ b/wa/bootstrap/src/main/java/org/apache/syncope/wa/bootstrap/SyncopeWAPropertySourceLocator.java
@@ -80,6 +80,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         syncopeProps.setUrl(conf.getUrl());
 
         casProperties.getAuthn().setSyncope(syncopeProps);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapStaticAuthModuleConf(
@@ -96,6 +97,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         staticProps.setUsers(users);
 
         casProperties.getAuthn().setAccept(staticProps);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapLdapAuthModuleConf(
@@ -115,6 +117,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         ldapProps.setPrincipalAttributeList(conf.getPrincipalAttributeList());
 
         casProperties.getAuthn().getLdap().add(ldapProps);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapGoogleMfaAuthModuleConf(
@@ -132,6 +135,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.setWindowSize(conf.getWindowSize());
 
         casProperties.getAuthn().getMfa().setGauth(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapU2fAuthModuleConf(
@@ -148,6 +152,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.setExpireRegistrationsTimeUnit(TimeUnit.valueOf(conf.getExpireRegistrationsTimeUnit()));
 
         casProperties.getAuthn().getMfa().setU2f(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapJaasAuthModuleConf(
@@ -165,6 +170,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.setRealm(conf.getRealm());
 
         casProperties.getAuthn().getJaas().add(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapJdbcAuthModuleConf(
@@ -187,6 +193,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.setPrincipalAttributeList(conf.getPrincipalAttributeList());
 
         casProperties.getAuthn().getJdbc().getQuery().add(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapOidcAuthModuleConf(
@@ -211,6 +218,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         client.setGeneric(props);
 
         casProperties.getAuthn().getPac4j().getOidc().add(client);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapRadiusAuthModuleConf(
@@ -239,6 +247,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.getServer().setRetries(conf.getRetries());
 
         casProperties.getAuthn().setRadius(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     private static void mapSaml2IdPAuthModuleConf(
@@ -258,13 +267,11 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
         props.setIdentityProviderMetadataPath(conf.getIdentityProviderMetadataPath());
         props.setKeystoreAlias(conf.getKeystoreAlias());
         props.setKeystorePassword(conf.getKeystorePassword());
-        props.setKeystorePath(conf.getKeystorePath());
         props.setMaximumAuthenticationLifetime(conf.getMaximumAuthenticationLifetime());
         props.setNameIdPolicyFormat(conf.getNameIdPolicyFormat());
         props.setPrivateKeyPassword(conf.getPrivateKeyPassword());
         props.setProviderName(conf.getProviderName());
         props.setServiceProviderEntityId(conf.getServiceProviderEntityId());
-        props.setServiceProviderMetadataPath(conf.getServiceProviderMetadataPath());
         props.setSignatureAlgorithms(conf.getSignatureAlgorithms());
         props.setSignatureCanonicalizationAlgorithm(conf.getSignatureCanonicalizationAlgorithm());
         props.setSignatureReferenceDigestMethods(conf.getSignatureReferenceDigestMethods());
@@ -274,6 +281,7 @@ public class SyncopeWAPropertySourceLocator implements PropertySourceLocator {
                 : TriStateBoolean.valueOf(conf.getNameIdPolicyAllowCreate().toUpperCase()));
 
         casProperties.getAuthn().getPac4j().getSaml().add(props);
+        LOG.debug("Added {} to WA configuration", conf.getName());
     }
 
     @Override
diff --git a/wa/starter/pom.xml b/wa/starter/pom.xml
index 180672f..18d92b4 100644
--- a/wa/starter/pom.xml
+++ b/wa/starter/pom.xml
@@ -192,6 +192,10 @@ under the License.
     </dependency>
     <dependency>
       <groupId>org.apereo.cas</groupId>
+      <artifactId>cas-server-support-pac4j-core-clients</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apereo.cas</groupId>
       <artifactId>cas-server-support-consent-webflow</artifactId>
     </dependency>
     <dependency>
@@ -240,6 +244,10 @@ under the License.
 
     <dependency>
       <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
       <artifactId>bcpkix-jdk15on</artifactId>
     </dependency>
     
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
index 058274a..e82294c 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAApplication.java
@@ -18,11 +18,9 @@
  */
 package org.apache.syncope.wa.starter;
 
-import org.apache.commons.lang.StringUtils;
 import org.apereo.cas.configuration.CasConfigurationProperties;
 import org.apereo.cas.configuration.CasConfigurationPropertiesValidator;
-import org.apereo.cas.util.AsciiArtUtils;
-import org.apereo.cas.util.DateTimeUtils;
+
 import org.quartz.JobBuilder;
 import org.quartz.JobDetail;
 import org.quartz.JobKey;
@@ -57,6 +55,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
+
 import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.Date;
@@ -96,17 +95,8 @@ public class SyncopeWAApplication extends SpringBootServletInitializer {
         new SpringApplicationBuilder(SyncopeWAApplication.class).run(args);
     }
 
-    private static void advertiseReady(final ApplicationReadyEvent event) {
-        AsciiArtUtils.printAsciiArtReady(LOG, StringUtils.EMPTY);
-        LOG.info("Ready to process requests @ [{}]", DateTimeUtils.zonedDateTimeOf(event.getTimestamp()));
-    }
-
     private static void validateConfiguration(final ApplicationReadyEvent event) {
-        if (!Boolean.getBoolean("SKIP_CONFIG_VALIDATION")) {
-            CasConfigurationPropertiesValidator validator =
-                    new CasConfigurationPropertiesValidator(event.getApplicationContext());
-            validator.validate();
-        }
+        new CasConfigurationPropertiesValidator(event.getApplicationContext()).validate();
     }
 
     /**
@@ -118,7 +108,6 @@ public class SyncopeWAApplication extends SpringBootServletInitializer {
     public void handleApplicationReadyEvent(final ApplicationReadyEvent event) {
         validateConfiguration(event);
         scheduleJobToRefreshContext();
-        advertiseReady(event);
     }
 
     private void scheduleJobToRefreshContext() {
@@ -131,9 +120,8 @@ public class SyncopeWAApplication extends SpringBootServletInitializer {
             JobDetail job = JobBuilder.newJob(SyncopeWARefreshContextJob.class).
                     withIdentity(jobKey).
                     build();
-
+            LOG.info("Scheduled job to refresh application context @ [{}]", date);
             scheduler.getScheduler().scheduleJob(job, trigger);
-
         } catch (final SchedulerException e) {
             throw new RuntimeException("Could not schedule refresh job", e);
         }
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
index 62e2d7b..2a85826 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWAConfiguration.java
@@ -18,9 +18,17 @@
  */
 package org.apache.syncope.wa.starter;
 
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
+import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
+import org.apereo.cas.configuration.CasConfigurationProperties;
+import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
+import org.apereo.cas.services.ServiceRegistryListener;
+import org.apereo.cas.support.pac4j.authentication.DelegatedClientFactoryCustomizer;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
+import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
+import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
+import org.apereo.cas.support.saml.idp.metadata.writer.SamlIdPCertificateAndKeyWriter;
+import org.apereo.cas.util.crypto.CipherExecutor;
+
 import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStart;
 import org.apache.syncope.common.keymaster.client.api.startstop.KeymasterStop;
@@ -34,17 +42,10 @@ import org.apache.syncope.wa.starter.mapping.AuthMapper;
 import org.apache.syncope.wa.starter.mapping.ClientAppMapFor;
 import org.apache.syncope.wa.starter.mapping.ClientAppMapper;
 import org.apache.syncope.wa.starter.mapping.RegisteredServiceMapper;
+import org.apache.syncope.wa.starter.pac4j.saml.SyncopeWASAML2ClientCustomizer;
 import org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataGenerator;
 import org.apache.syncope.wa.starter.saml.idp.metadata.RestfulSamlIdPMetadataLocator;
-import org.apereo.cas.audit.AuditTrailExecutionPlanConfigurer;
-import org.apereo.cas.configuration.CasConfigurationProperties;
-import org.apereo.cas.services.ServiceRegistryExecutionPlanConfigurer;
-import org.apereo.cas.services.ServiceRegistryListener;
-import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
-import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
-import org.apereo.cas.support.saml.idp.metadata.locator.SamlIdPMetadataLocator;
-import org.apereo.cas.support.saml.idp.metadata.writer.SamlIdPCertificateAndKeyWriter;
-import org.apereo.cas.util.crypto.CipherExecutor;
+import org.pac4j.core.client.Client;
 import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
@@ -55,6 +56,10 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.io.ResourceLoader;
 
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
 @Configuration
 public class SyncopeWAConfiguration {
 
@@ -114,19 +119,19 @@ public class SyncopeWAConfiguration {
         });
 
         return new RegisteredServiceMapper(
-                authPolicyConfMappers,
-                accessPolicyConfMappers,
-                attrReleasePolicyConfMappers,
-                clientAppTOMappers);
+            authPolicyConfMappers,
+            accessPolicyConfMappers,
+            attrReleasePolicyConfMappers,
+            clientAppTOMappers);
     }
 
     @Autowired
     @Bean
     public ServiceRegistryExecutionPlanConfigurer syncopeServiceRegistryConfigurer(
-            final WARestClient restClient, final RegisteredServiceMapper registeredServiceMapper) {
+        final WARestClient restClient, final RegisteredServiceMapper registeredServiceMapper) {
 
         SyncopeServiceRegistry registry = new SyncopeServiceRegistry(
-                restClient, registeredServiceMapper, applicationContext, serviceRegistryListeners);
+            restClient, registeredServiceMapper, applicationContext, serviceRegistryListeners);
         return plan -> plan.registerServiceRegistry(registry);
     }
 
@@ -134,13 +139,13 @@ public class SyncopeWAConfiguration {
     @Bean
     public SamlIdPMetadataGenerator samlIdPMetadataGenerator(final WARestClient restClient) {
         SamlIdPMetadataGeneratorConfigurationContext context =
-                SamlIdPMetadataGeneratorConfigurationContext.builder().
-                        samlIdPMetadataLocator(samlIdPMetadataLocator(restClient)).
-                        samlIdPCertificateAndKeyWriter(samlSelfSignedCertificateWriter.getObject()).
-                        resourceLoader(resourceLoader).
-                        casProperties(casProperties).
-                        metadataCipherExecutor(CipherExecutor.noOpOfStringToString()).
-                        build();
+            SamlIdPMetadataGeneratorConfigurationContext.builder().
+                samlIdPMetadataLocator(samlIdPMetadataLocator(restClient)).
+                samlIdPCertificateAndKeyWriter(samlSelfSignedCertificateWriter.getObject()).
+                resourceLoader(resourceLoader).
+                casProperties(casProperties).
+                metadataCipherExecutor(CipherExecutor.noOpOfStringToString()).
+                build();
         return new RestfulSamlIdPMetadataGenerator(context, restClient);
     }
 
@@ -156,6 +161,12 @@ public class SyncopeWAConfiguration {
         return plan -> plan.registerAuditTrailManager(new SyncopeWAAuditTrailManager(restClient));
     }
 
+    @Autowired
+    @Bean
+    public DelegatedClientFactoryCustomizer<Client> delegatedClientCustomizer(final WARestClient restClient) {
+        return new SyncopeWASAML2ClientCustomizer(restClient);
+    }
+
     @Bean
     public KeymasterStart keymasterStart() {
         return new KeymasterStart(NetworkService.Type.WA);
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWARefreshContextJob.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWARefreshContextJob.java
index 9d5a1cc..8f4edf7 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWARefreshContextJob.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/SyncopeWARefreshContextJob.java
@@ -19,8 +19,11 @@
 package org.apache.syncope.wa.starter;
 
 import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.apache.commons.lang.StringUtils;
 import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGenerator;
 import org.apereo.cas.support.saml.services.idp.metadata.SamlIdPMetadataDocument;
+import org.apereo.cas.util.AsciiArtUtils;
+
 import org.quartz.Job;
 import org.quartz.JobExecutionContext;
 import org.slf4j.Logger;
@@ -46,19 +49,27 @@ public class SyncopeWARefreshContextJob implements Job {
     @Override
     public void execute(final JobExecutionContext jobExecutionContext) throws JobExecutionException {
         try {
-            LOG.debug("Refreshing WA application context");
+            LOG.debug("Attempting to refresh WA application context");
             if (!WARestClient.isReady()) {
                 LOG.debug("Syncope client is not yet ready");
                 throw new RuntimeException("Syncope core is not yet ready to access requests");
             }
             contextRefresher.refresh();
+            LOG.info("Refreshed application context to bootstrap property sources, etc...");
 
             LOG.info("Generating SAML2 IdP metadata metadata");
             SamlIdPMetadataDocument document = metadataGenerator.generate(Optional.empty());
             LOG.info("Generated SAML2 IdP metadata for {}", document.getAppliesTo());
 
+            advertiseReady();
+
         } catch (RuntimeException e) {
             throw new JobExecutionException("While generating SAML2 IdP metadata", e);
         }
     }
+
+    private static void advertiseReady() {
+        AsciiArtUtils.printAsciiArtReady(LOG, StringUtils.EMPTY);
+        LOG.info("Ready to process requests");
+    }
 }
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizer.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizer.java
new file mode 100644
index 0000000..0dfc403
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apereo.cas.support.pac4j.authentication.DelegatedClientFactoryCustomizer;
+
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.pac4j.core.client.Client;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SyncopeWASAML2ClientCustomizer implements DelegatedClientFactoryCustomizer<Client> {
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWASAML2ClientCustomizer.class);
+
+    private final WARestClient restClient;
+
+    public SyncopeWASAML2ClientCustomizer(final WARestClient restClient) {
+        this.restClient = restClient;
+    }
+
+    @Override
+    public void customize(final Client client) {
+        if (client instanceof SAML2Client) {
+            LOG.debug("Customizing SAML2 client {}", client.getName());
+            final SAML2Client saml2Client = (SAML2Client) client;
+            SAML2Configuration configuration = saml2Client.getConfiguration();
+            configuration.setKeystoreGenerator(new SyncopeWASAML2ClientKeystoreGenerator(restClient, saml2Client));
+            configuration.setMetadataGenerator(new SyncopeWASAML2ClientMetadataGenerator(restClient, saml2Client));
+        }
+    }
+}
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGenerator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGenerator.java
new file mode 100644
index 0000000..77f84f6
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGenerator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.metadata.keystore.BaseSAML2KeystoreGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+
+public class SyncopeWASAML2ClientKeystoreGenerator extends BaseSAML2KeystoreGenerator {
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWASAML2ClientKeystoreGenerator.class);
+
+    private final WARestClient restClient;
+
+    private final SAML2Client saml2Client;
+
+    SyncopeWASAML2ClientKeystoreGenerator(final WARestClient restClient, final SAML2Client saml2Client) {
+        super(saml2Client.getConfiguration());
+        this.restClient = restClient;
+        this.saml2Client = saml2Client;
+    }
+
+    @Override
+    public boolean shouldGenerate() {
+        return true;
+    }
+
+    @Override
+    protected void store(final KeyStore ks, final X509Certificate certificate,
+                         final PrivateKey privateKey) throws Exception {
+        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            char[] password = saml2Configuration.getKeystorePassword().toCharArray();
+            ks.store(out, password);
+            out.flush();
+            String encodedKeystore = Base64.getEncoder().encodeToString(out.toByteArray());
+            LOG.debug("Encoded keystore {}", encodedKeystore);
+
+            SAML2SPKeystoreService keystoreService = restClient.getSyncopeClient().
+                getService(SAML2SPKeystoreService.class);
+
+            SAML2SPKeystoreTO keystoreTO = new SAML2SPKeystoreTO.Builder().
+                keystore(encodedKeystore).
+                owner(saml2Client.getName()).
+                build();
+
+            LOG.debug("Storing keystore {}", keystoreTO);
+            Response response = keystoreService.set(keystoreTO);
+            if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
+                LOG.info("Stored keystore for SAML2 SP {}", saml2Client.getName());
+            } else if (response.getStatus() == Response.Status.CONFLICT.getStatusCode()) {
+                LOG.info("Stored keystore for SAML2 SP {} already exists", saml2Client.getName());
+            } else {
+                LOG.error("Unexpected response when storing SAML2 SP keystore: {}\n{}",
+                    response.getStatus(), response.getHeaders());
+                SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+                sce.getElements().add("Unexpected response when storing SAML2 SP keystore");
+                throw sce;
+            }
+        }
+    }
+
+    @Override
+    public InputStream retrieve() throws Exception {
+        try {
+            SAML2SPKeystoreService keystoreService = restClient.getSyncopeClient().
+                getService(SAML2SPKeystoreService.class);
+            SAML2SPKeystoreTO keystoreTO = keystoreService.getByOwner(saml2Client.getName());
+            LOG.debug("Retrieved keystore {}", keystoreTO);
+            byte[] decode = Base64.getDecoder().decode(keystoreTO.getKeystore());
+            return new ByteArrayInputStream(decode);
+        } catch (final Exception e) {
+            final String message = "Unable to fetch SAML2 SP keystore for " + saml2Client.getName();
+            LOG.error(message, e);
+            throw new Exception(message);
+        }
+    }
+}
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGenerator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGenerator.java
new file mode 100644
index 0000000..9ba283c
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGenerator.java
@@ -0,0 +1,77 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.opensaml.saml.metadata.resolver.impl.AbstractBatchMetadataResolver;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.metadata.BaseSAML2MetadataGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+
+import javax.ws.rs.core.Response;
+
+public class SyncopeWASAML2ClientMetadataGenerator extends BaseSAML2MetadataGenerator {
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWASAML2ClientMetadataGenerator.class);
+
+    private final WARestClient restClient;
+
+    private final SAML2Client saml2Client;
+
+    SyncopeWASAML2ClientMetadataGenerator(final WARestClient restClient, final SAML2Client saml2Client) {
+        this.restClient = restClient;
+        this.saml2Client = saml2Client;
+    }
+
+    @Override
+    protected AbstractBatchMetadataResolver createMetadataResolver(final Resource metadataResource) {
+        return new SyncopeWASAML2MetadataResolver(restClient, saml2Client);
+    }
+
+    @Override
+    public boolean storeMetadata(final String metadata, final Resource resource, final boolean force) throws Exception {
+        SAML2SPMetadataService metadataService = restClient.getSyncopeClient().
+            getService(SAML2SPMetadataService.class);
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO.Builder().
+            metadata(metadata).
+            owner(saml2Client.getName()).
+            build();
+        LOG.debug("Storing metadata {}", metadataTO);
+        Response response = metadataService.set(metadataTO);
+        if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
+            LOG.info("Stored metadata for SAML2 SP {}", saml2Client.getName());
+            return true;
+        }
+        if (response.getStatus() == Response.Status.CONFLICT.getStatusCode()) {
+            LOG.info("Stored metadata for SAML2 SP {} already exists", saml2Client.getName());
+            return true;
+        }
+        LOG.error("Unexpected response when storing SAML2 SP metadata: {}\n{}",
+            response.getStatus(), response.getHeaders());
+        SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
+        sce.getElements().add("Unexpected response when storing SAML2 SP metadata");
+        throw sce;
+    }
+}
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolver.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolver.java
new file mode 100644
index 0000000..da71d2a
--- /dev/null
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolver.java
@@ -0,0 +1,63 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.opensaml.saml.metadata.resolver.impl.AbstractReloadingMetadataResolver;
+import org.pac4j.saml.client.SAML2Client;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+
+public class SyncopeWASAML2MetadataResolver extends AbstractReloadingMetadataResolver {
+    private static final Logger LOG = LoggerFactory.getLogger(SyncopeWASAML2MetadataResolver.class);
+
+    private final WARestClient restClient;
+
+    private final SAML2Client saml2Client;
+
+    SyncopeWASAML2MetadataResolver(final WARestClient restClient, final SAML2Client saml2Client) {
+        this.restClient = restClient;
+        this.saml2Client = saml2Client;
+    }
+
+    @Override
+    protected String getMetadataIdentifier() {
+        return saml2Client.getName();
+    }
+
+    @Override
+    protected byte[] fetchMetadata() throws ResolverException {
+        try {
+            SAML2SPMetadataService metadataService = restClient.getSyncopeClient().
+                getService(SAML2SPMetadataService.class);
+            SAML2SPMetadataTO metadataTO = metadataService.getByOwner(saml2Client.getName());
+            return metadataTO.getMetadata().getBytes(StandardCharsets.UTF_8);
+        } catch (final Exception e) {
+            final String message = "Unable to fetch SP metadata for " + saml2Client.getName();
+            LOG.error(message, e);
+            throw new ResolverException(message);
+        }
+    }
+}
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
index ceeff87..dd33e68 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataGenerator.java
@@ -21,7 +21,7 @@ package org.apache.syncope.wa.starter.saml.idp.metadata;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2IdPMetadataService;
 import org.apache.syncope.wa.bootstrap.WARestClient;
 import org.apereo.cas.support.saml.idp.metadata.generator.BaseSamlIdPMetadataGenerator;
 import org.apereo.cas.support.saml.idp.metadata.generator.SamlIdPMetadataGeneratorConfigurationContext;
diff --git a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
index d2a1634..eb2c220 100644
--- a/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
+++ b/wa/starter/src/main/java/org/apache/syncope/wa/starter/saml/idp/metadata/RestfulSamlIdPMetadataLocator.java
@@ -22,7 +22,7 @@ import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.SAML2IdPMetadataTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
-import org.apache.syncope.common.rest.api.service.SAML2IdPMetadataService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2IdPMetadataService;
 import org.apache.syncope.wa.bootstrap.WARestClient;
 import org.apereo.cas.support.saml.idp.metadata.locator.AbstractSamlIdPMetadataLocator;
 import org.apereo.cas.support.saml.services.SamlRegisteredService;
@@ -59,7 +59,7 @@ public class RestfulSamlIdPMetadataLocator extends AbstractSamlIdPMetadataLocato
         try {
             LOG.info("Locating SAML2 IdP metadata document");
             SAML2IdPMetadataTO saml2IdPMetadataTO = getSyncopeClient().getService(SAML2IdPMetadataService.class).
-                    get(getAppliesToFor(registeredService));
+                getByOwner(getAppliesToFor(registeredService));
 
             if (saml2IdPMetadataTO == null) {
                 LOG.warn("No SAML2 IdP metadata document obtained from core");
diff --git a/wa/starter/src/main/resources/wa.properties b/wa/starter/src/main/resources/wa.properties
index c98a6e7..8a344e6 100644
--- a/wa/starter/src/main/resources/wa.properties
+++ b/wa/starter/src/main/resources/wa.properties
@@ -21,7 +21,7 @@ useGZIPCompression=true
 
 # Conf directories
 conf.directory=${conf.directory}
-cas.standalone.configurationDirectory=${conf.directory}
+cas.standalone.configuration-directory=${conf.directory}
 cas.authn.oidc.jwks.jwksFile=file:${conf.directory}/oidc.keystore
 
 cas.server.name=http://localhost:8080
diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/BaseSyncopeWASAML2ClientTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/BaseSyncopeWASAML2ClientTest.java
new file mode 100644
index 0000000..253b6f8
--- /dev/null
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/BaseSyncopeWASAML2ClientTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.bouncycastle.asn1.ASN1EncodableVector;
+import org.bouncycastle.asn1.ASN1Encoding;
+import org.bouncycastle.asn1.ASN1Integer;
+import org.bouncycastle.asn1.DERBitString;
+import org.bouncycastle.asn1.DERNull;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.asn1.x509.TBSCertificate;
+import org.bouncycastle.asn1.x509.Time;
+import org.bouncycastle.asn1.x509.V3TBSCertificateGenerator;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.Signature;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.util.Base64;
+import java.util.Date;
+
+public abstract class BaseSyncopeWASAML2ClientTest {
+
+    protected static Certificate createSelfSignedCert(final KeyPair keyPair) throws Exception {
+        final X500Name dn = new X500Name("cn=Unknown");
+        final V3TBSCertificateGenerator certGen = new V3TBSCertificateGenerator();
+
+        certGen.setSerialNumber(new ASN1Integer(BigInteger.valueOf(1)));
+        certGen.setIssuer(dn);
+        certGen.setSubject(dn);
+        certGen.setStartDate(new Time(new Date(System.currentTimeMillis() - 1000L)));
+
+        final Date expiration = new Date(System.currentTimeMillis() + 100000);
+        certGen.setEndDate(new Time(expiration));
+
+        final AlgorithmIdentifier sigAlgID = new AlgorithmIdentifier(PKCSObjectIdentifiers.sha1WithRSAEncryption, DERNull.INSTANCE);
+        certGen.setSignature(sigAlgID);
+        certGen.setSubjectPublicKeyInfo(SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
+
+        final Signature sig = Signature.getInstance("SHA1WithRSA");
+        sig.initSign(keyPair.getPrivate());
+        sig.update(certGen.generateTBSCertificate().getEncoded(ASN1Encoding.DER));
+
+        final TBSCertificate tbsCert = certGen.generateTBSCertificate();
+        final ASN1EncodableVector v = new ASN1EncodableVector();
+
+        v.add(tbsCert);
+        v.add(sigAlgID);
+        v.add(new DERBitString(sig.sign()));
+
+        final Certificate cert = CertificateFactory.getInstance("X.509")
+            .generateCertificate(new ByteArrayInputStream(new DERSequence(v).getEncoded(ASN1Encoding.DER)));
+        cert.verify(keyPair.getPublic());
+        return cert;
+    }
+
+    protected static SAML2Client getSAML2Client() {
+        SAML2Configuration saml2Configuration = new SAML2Configuration();
+        saml2Configuration.setKeystorePassword("password");
+        saml2Configuration.setPrivateKeyPassword("password");
+        saml2Configuration.setKeystoreAlias("Syncope");
+        saml2Configuration.setIdentityProviderMetadataResource(new ClassPathResource("idp-metadata.xml"));
+        SAML2Client client = new SAML2Client(saml2Configuration);
+        client.setCallbackUrl("https://syncope.apache.org");
+        return client;
+    }
+
+    protected static KeyStore getKeystore() throws Exception {
+        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        char[] pwdArray = "password".toCharArray();
+        ks.load(null, pwdArray);
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+        keyPairGenerator.initialize(4096);
+        KeyPair keyPair = keyPairGenerator.generateKeyPair();
+        Certificate certificate = createSelfSignedCert(keyPair);
+        ks.setKeyEntry("Syncope", keyPair.getPrivate(), "password".toCharArray(), new Certificate[]{certificate});
+        return ks;
+    }
+
+    protected static String getKeystoreAsString() throws Exception {
+        char[] pwdArray = "password".toCharArray();
+        try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) {
+            getKeystore().store(fos, pwdArray);
+            fos.flush();
+            return Base64.getEncoder().encodeToString(fos.toByteArray());
+        }
+    }
+}
diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizerTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizerTest.java
new file mode 100644
index 0000000..f752fdd
--- /dev/null
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientCustomizerTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.junit.jupiter.api.Test;
+import org.pac4j.saml.client.SAML2Client;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SyncopeWASAML2ClientCustomizerTest extends BaseSyncopeWASAML2ClientTest {
+
+    @Test
+    public void customize() throws Exception {
+
+        SAML2SPKeystoreTO keystoreTO = new SAML2SPKeystoreTO.Builder()
+            .keystore(getKeystoreAsString())
+            .owner("CAS")
+            .build();
+        SAML2SPKeystoreService saml2SPKeystoreService = mock(SAML2SPKeystoreService.class);
+        when(saml2SPKeystoreService.getByOwner(anyString())).thenReturn(keystoreTO);
+        when(saml2SPKeystoreService.set(any())).thenReturn(Response.created(new URI("http://localhost:9080/syncop-wa")).build());
+
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO.Builder()
+            .owner("Syncope")
+            .metadata(IOUtils.toString(new ClassPathResource("sp-metadata.xml").getInputStream(), StandardCharsets.UTF_8))
+            .build();
+
+        SAML2SPMetadataService saml2SPMetadataService = mock(SAML2SPMetadataService.class);
+        when(saml2SPMetadataService.getByOwner(anyString())).thenReturn(metadataTO);
+        when(saml2SPMetadataService.set(any())).thenReturn(Response.created(new URI("http://localhost:9080/syncop-wa")).build());
+
+        WARestClient restClient = mock(WARestClient.class);
+
+        SyncopeClient syncopeClient = mock(SyncopeClient.class);
+        when(syncopeClient.getService(SAML2SPKeystoreService.class)).thenReturn(saml2SPKeystoreService);
+        when(syncopeClient.getService(SAML2SPMetadataService.class)).thenReturn(saml2SPMetadataService);
+        when(restClient.getSyncopeClient()).thenReturn(syncopeClient);
+
+        SyncopeWASAML2ClientCustomizer customizer = new SyncopeWASAML2ClientCustomizer(restClient);
+        SAML2Client client = getSAML2Client();
+        customizer.customize(client);
+        client.init();
+        assertTrue(client.getConfiguration().getKeystoreGenerator() instanceof SyncopeWASAML2ClientKeystoreGenerator);
+        assertTrue(client.getConfiguration().toMetadataGenerator() instanceof SyncopeWASAML2ClientMetadataGenerator);
+    }
+}
diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGeneratorTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGeneratorTest.java
new file mode 100644
index 0000000..8c50d22
--- /dev/null
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientKeystoreGeneratorTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.syncope.client.lib.SyncopeClient;
+
+import org.apache.syncope.common.lib.to.SAML2SPKeystoreTO;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPKeystoreService;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+import org.pac4j.saml.exceptions.SAMLException;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.metadata.keystore.SAML2KeystoreGenerator;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.ws.rs.core.Response;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SyncopeWASAML2ClientKeystoreGeneratorTest extends BaseSyncopeWASAML2ClientTest {
+    private static WARestClient getWaRestClient(final Response response) throws Exception {
+        WARestClient restClient = mock(WARestClient.class);
+
+        SAML2SPKeystoreTO keystoreTO = new SAML2SPKeystoreTO.Builder()
+            .keystore(getKeystoreAsString())
+            .owner("CAS")
+            .build();
+        SAML2SPKeystoreService saml2SPKeystoreService = mock(SAML2SPKeystoreService.class);
+        when(saml2SPKeystoreService.getByOwner(anyString())).thenReturn(keystoreTO);
+        when(saml2SPKeystoreService.set(any())).thenReturn(response);
+
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO.Builder()
+            .owner("Syncope")
+            .metadata(IOUtils.toString(new ClassPathResource("sp-metadata.xml").getInputStream(), StandardCharsets.UTF_8))
+            .build();
+
+        SAML2SPMetadataService saml2SPMetadataService = mock(SAML2SPMetadataService.class);
+        when(saml2SPMetadataService.getByOwner(anyString())).thenReturn(metadataTO);
+        when(saml2SPMetadataService.set(any())).thenReturn(response);
+
+        SyncopeClient syncopeClient = mock(SyncopeClient.class);
+
+        when(syncopeClient.getService(SAML2SPMetadataService.class)).thenReturn(saml2SPMetadataService);
+        when(syncopeClient.getService(SAML2SPKeystoreService.class)).thenReturn(saml2SPKeystoreService);
+
+        when(restClient.getSyncopeClient()).thenReturn(syncopeClient);
+        return restClient;
+    }
+
+    @Test
+    public void generate() throws Exception {
+        SAML2Client client = getSAML2Client();
+        SAML2KeystoreGenerator generator1 = new SyncopeWASAML2ClientKeystoreGenerator(
+            getWaRestClient(Response.created(new URI("http://localhost:9080/syncop-wa")).build()), client);
+        assertDoesNotThrow(new Executable() {
+            @Override
+            public void execute() throws Throwable {
+                generator1.generate();
+            }
+        });
+        SAML2KeystoreGenerator generator2 = new SyncopeWASAML2ClientKeystoreGenerator(getWaRestClient(Response.serverError().build()), client);
+        assertThrows(SAMLException.class, () -> generator2.generate());
+    }
+}
diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGeneratorTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGeneratorTest.java
new file mode 100644
index 0000000..11d01ae
--- /dev/null
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2ClientMetadataGeneratorTest.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.wa.starter.pac4j.saml;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.junit.jupiter.api.Test;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.metadata.SAML2MetadataGenerator;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.ws.rs.core.Response;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SyncopeWASAML2ClientMetadataGeneratorTest extends BaseSyncopeWASAML2ClientTest {
+    private static WARestClient getWaRestClient(final Response response) throws IOException {
+        WARestClient restClient = mock(WARestClient.class);
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO.Builder()
+            .owner("Syncope")
+            .metadata(IOUtils.toString(new ClassPathResource("sp-metadata.xml").getInputStream(), StandardCharsets.UTF_8))
+            .build();
+
+        SAML2SPMetadataService saml2SPMetadataService = mock(SAML2SPMetadataService.class);
+        when(saml2SPMetadataService.getByOwner(anyString())).thenReturn(metadataTO);
+        when(saml2SPMetadataService.set(any())).thenReturn(response);
+
+        SyncopeClient syncopeClient = mock(SyncopeClient.class);
+        when(syncopeClient.getService(SAML2SPMetadataService.class)).thenReturn(saml2SPMetadataService);
+        when(restClient.getSyncopeClient()).thenReturn(syncopeClient);
+        return restClient;
+    }
+
+    @Test
+    public void storeMetadata() throws Exception {
+        SAML2Client client = getSAML2Client();
+        String keystoreFile = File.createTempFile("keystore", "jks").getCanonicalPath();
+        client.getConfiguration().setKeystoreResourceFilepath(keystoreFile);
+
+        SAML2MetadataGenerator generator = new SyncopeWASAML2ClientMetadataGenerator(
+            getWaRestClient(Response.created(new URI("http://localhost:9080/syncop-wa")).build()), client);
+        EntityDescriptor entityDescriptor = generator.buildEntityDescriptor();
+        String metadata = generator.getMetadata(entityDescriptor);
+        assertNotNull(generator.storeMetadata(metadata, null, false));
+    }
+
+    @Test
+    public void storeMetadataFails() throws Exception {
+        SAML2Client client = getSAML2Client();
+        String keystoreFile = File.createTempFile("keystore", "jks").getCanonicalPath();
+        client.getConfiguration().setKeystoreResourceFilepath(keystoreFile);
+        WARestClient restClient = getWaRestClient(Response.serverError().build());
+        SAML2MetadataGenerator generator = new SyncopeWASAML2ClientMetadataGenerator(restClient, client);
+        EntityDescriptor entityDescriptor = generator.buildEntityDescriptor();
+        String metadata = generator.getMetadata(entityDescriptor);
+        assertThrows(SyncopeClientException.class, () -> generator.storeMetadata(metadata, null, false));
+    }
+}
diff --git a/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolverTest.java b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolverTest.java
new file mode 100644
index 0000000..b7a630f
--- /dev/null
+++ b/wa/starter/src/test/java/org/apache/syncope/wa/starter/pac4j/saml/SyncopeWASAML2MetadataResolverTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.wa.starter.pac4j.saml;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.common.lib.to.SAML2SPMetadataTO;
+import org.apache.syncope.common.rest.api.service.wa.SAML2SPMetadataService;
+import org.apache.syncope.wa.bootstrap.WARestClient;
+import org.junit.jupiter.api.Test;
+import org.pac4j.saml.client.SAML2Client;
+import org.springframework.core.io.ClassPathResource;
+
+import javax.ws.rs.core.Response;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SyncopeWASAML2MetadataResolverTest extends BaseSyncopeWASAML2ClientTest {
+    @Test
+    public void fetchMetadata() throws Exception {
+        SAML2Client client = getSAML2Client();
+        String keystoreFile = File.createTempFile("keystore", "jks").getCanonicalPath();
+        client.getConfiguration().setKeystoreResourceFilepath(keystoreFile);
+        WARestClient restClient = mock(WARestClient.class);
+
+        SAML2SPMetadataTO metadataTO = new SAML2SPMetadataTO.Builder()
+            .owner("Syncope")
+            .metadata(IOUtils.toString(new ClassPathResource("sp-metadata.xml").getInputStream(), StandardCharsets.UTF_8))
+            .build();
+
+        SAML2SPMetadataService saml2SPMetadataService = mock(SAML2SPMetadataService.class);
+        when(saml2SPMetadataService.getByOwner(anyString())).thenReturn(metadataTO);
+        when(saml2SPMetadataService.set(any())).thenReturn(Response.created(new URI("http://localhost:9080/syncop-wa")).build());
+
+        SyncopeClient syncopeClient = mock(SyncopeClient.class);
+        when(syncopeClient.getService(SAML2SPMetadataService.class)).thenReturn(saml2SPMetadataService);
+        when(restClient.getSyncopeClient()).thenReturn(syncopeClient);
+
+        SyncopeWASAML2MetadataResolver resolver = new SyncopeWASAML2MetadataResolver(restClient, client);
+        assertNotNull(resolver.fetchMetadata());
+    }
+}
diff --git a/wa/starter/src/test/resources/idp-metadata.xml b/wa/starter/src/test/resources/idp-metadata.xml
new file mode 100644
index 0000000..6e8f2f9
--- /dev/null
+++ b/wa/starter/src/test/resources/idp-metadata.xml
@@ -0,0 +1,144 @@
+<?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.
+-->
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" entityID="https://syncope.apache.org/idp">
+    <IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
++Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <KeyDescriptor use="encryption">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUKymtgciRE6pWwDfrsI58qL9pQMgwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCKrT805sny3vlaYjTn
++m6m3VbUoStvnacgwHH+orNFhHoV1HP2ndoH5BsEDB9tQYuyRbGUm/nYVOSHayzB
+G3bzMGdU7woO6rsCqpHuxUyEvojd/y/N/r9jMzeBOCu0KDBTrn3BJhnGSwSTfhOS
+3r20JFmDuTkHmabRs7ro0BvDaQ29jh38ro1iwB4E/4mqb1zYP13NI3ooErN/o6pl
+XKpnFY37bDDOyOuocjN9tfPNIANNFKah0HjWOP0Nso0D1g6jHOSzmOw/Yxg61vBk
+qOD4aKhLYPAxsXRl80nDrwTnm3/9xLQj9D3uLAtDLnn9pSqn3jCLxsxsHfKL/zkB
+IKEBAgMBAAGjYzBhMB0GA1UdDgQWBBSrPjAgCJIHYmsofDcDIPzEhnYxmTBABgNV
+HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAI8MlofbE0tbq8ez2d0Lq
+Syhp4Q/shMEwjqcDarOwR+ACB9McOannUpAG7TCDp8Ch5E/V1B0Uo/5DF2tAzB1y
+7sgAmy2mY9/mFhMYpOqTCagufwewaMkn9n7ETzC/6vQEjYrjiNyNR0F3UQQz2bhe
+ROM3YuKctuOnMthc+ZE7vn+AXCGumRHBhyCaYdzfeUh7id+yrd9B51+o3iF4eu6w
+zJi5z7FMCS6I4PSc/uWYDw1ahzoPONjazWSEWGUibZaJYM3pJHkuwqyWKOFGVknH
+J1Qv4WCfSPb6eva94TZX0lkLM01C7NZObnfxY3fvJGcyFl8wlRTUYvuqM8md5CEp
+LA==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+
+        <!--
+        <ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" 
+                                   Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/ArtifactResolution" index="1"/>
+        -->
+        
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SLO"/>
+        <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SLO" />
+
+        <NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
+        <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign" Location="https://syncope.apache.org/cas/idp/profile/SAML2/POST-SimpleSign/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://syncope.apache.org/cas/idp/profile/SAML2/Redirect/SSO"/>
+        <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/ECP"/>
+    </IDPSSODescriptor>
+
+    <!--
+    <AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
+        <Extensions>
+            <shibmd:Scope regexp="false">example.net</shibmd:Scope>
+        </Extensions>
+        <KeyDescriptor use="signing">
+            <ds:KeyInfo>
+                <ds:X509Data>
+                    <ds:X509Certificate>MIIDLTCCAhWgAwIBAgIUVqwgQMQunB5UtoiiOqP1oQeg7lcwDQYJKoZIhvcNAQEL
+BQAwHjEcMBoGA1UEAwwTbW1vYXl5ZWQudW5pY29uLm5ldDAeFw0xOTExMDExNDQ3
+NDhaFw0zOTExMDExNDQ3NDhaMB4xHDAaBgNVBAMME21tb2F5eWVkLnVuaWNvbi5u
+ZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLXKHf93KZztJfCpNg
+R/ip6EdOp3Z52HmwT32QlOzeby+2prqbOanQcs5oEeXoz6cdzjwOO6isnqZ3ES7p
+BuVyuUoYVZyuXY6dsk6ANxeOXBRzGBS3ZemzYRQVmvQudNHUqdXpJelkFZvz3Au2
+I594V2PZjywtuGIUb+T7j+8hh6Srf8c/W/KmC3KLFfU2yDQrcjuhGv+0Py5ZUpXs
+EANs/d/AYV+LbMp3UtvWSOy8xGb+xxjS2KhTd53Oc6xsCgTPgTM5Y3DVA0ERNH+n
+ppngRi/t3NggIN0EKYAS6ZqJi1GBEVHFOoacebLSy/UQA8tYI170/gf03/OYwO2S
+9GATAgMBAAGjYzBhMB0GA1UdDgQWBBQxJh8NNf+qGJNZPlOItCWFQFY/wDBABgNV
+HREEOTA3ghNtbW9heXllZC51bmljb24ubmV0hiBtbW9heXllZC51bmljb24ubmV0
+L2lkcC9tZXRhZGF0YTANBgkqhkiG9w0BAQsFAAOCAQEAMMOb+f4Log69KUeAEvgh
+sWTjiZujvl44nY4roXofAoXYc3vos/p5JVwEtrxgTLdyTsz65kZtaRISRrUJ3k0n
+K22L2eXGa85qPhdKivRyNip5AMVi0zSXC6uhG50571Gy5UK/Rh3gvg7VM8GUFDHL
++Zay9ffV9lf0UVmFObA+PAe+HNY/dYRLIP9/pFW0+c1MmFtwCTrO4xbecfzA+Yde
+9dbaBjS4veOSvFKiaCOvsiIVEUt1J7NrqM5sgYvOR5Q5zv0G72pmzS8cuGe2UP7e
+i24oGm471cMDTLyFLYMCL8veHydcgfIV9z5g0PksV0kQL91r4XVkIp3iFZJ+TUBF
+zg==</ds:X509Certificate>
+                </ds:X509Data>
+            </ds:KeyInfo>
+        </KeyDescriptor>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://syncope.apache.org/cas/idp/profile/SAML1/SOAP/AttributeQuery"/>
+        <AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://syncope.apache.org/cas/idp/profile/SAML2/SOAP/AttributeQuery"/>
+    </AttributeAuthorityDescriptor>
+    -->
+    
+    <!--
+    <Organization>
+        <OrganizationName xml:lang="en">Institution Name</OrganizationName>
+        <OrganizationDisplayName xml:lang="en">Institution DisplayName</OrganizationDisplayName>
+        <OrganizationURL xml:lang="en">URL</OrganizationURL>
+    </Organization>
+    <ContactPerson contactType="administrative">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="technical">
+        <GivenName>John Smith</GivenName>
+        <EmailAddress>jsmith@example.org</EmailAddress>
+    </ContactPerson>
+    <ContactPerson contactType="support">
+        <GivenName>IT Services Support</GivenName>
+        <EmailAddress>support@example.org</EmailAddress>
+    </ContactPerson>
+    -->
+</EntityDescriptor>
diff --git a/wa/starter/src/test/resources/sp-metadata.xml b/wa/starter/src/test/resources/sp-metadata.xml
new file mode 100644
index 0000000..351d556
--- /dev/null
+++ b/wa/starter/src/test/resources/sp-metadata.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" ID="_lipmbmwyb3ujt0osvfect39lzglxnsdi7bxhvf5" entityID="urn:mace:saml:pac4j.org" validUntil="2038-08-10T19:43:10.846Z">
+    <md:Extensions xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport">
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha224"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
+        <alg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#dsa-sha1"/>
+    </md:Extensions>
+    <md:SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.0:protocol urn:oasis:names:tc:SAML:1.1:protocol">
+        <md:Extensions xmlns:init="urn:oasis:names:tc:SAML:profiles:SSO:request-init">
+            <init:RequestInitiator Binding="urn:oasis:names:tc:SAML:profiles:SSO:request-init" Location="https://mmoayyed.unicon.net:8443/cas/login?client_name=SAML2Client"/>
+        </md:Extensions>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
+        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
+        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mmoayyed.unicon.net:8443/cas/login?client_name=SAML2Client" index="0"/>
+    </md:SPSSODescriptor>
+</md:EntityDescriptor>